blob: 6bbfc2b1277a7e3b0db4bb4051ebef63fa4f6a9e [file] [log] [blame]
Misha Wagner566903a2018-10-02 10:50:12 +01001/*
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
17package com.android.internal.os;
18
19import android.annotation.Nullable;
20import android.os.Process;
21import android.util.Slog;
22
23import com.android.internal.annotations.VisibleForTesting;
Misha Wagner0c5edc362018-10-24 11:28:57 +010024import com.android.internal.util.Preconditions;
Misha Wagner566903a2018-10-02 10:50:12 +010025
26import java.io.IOException;
27import java.nio.file.DirectoryStream;
28import java.nio.file.Files;
29import java.nio.file.Path;
30import java.nio.file.Paths;
31import java.util.ArrayList;
Misha Wagnerdc797542018-10-29 13:21:34 +000032import java.util.function.Predicate;
Misha Wagner566903a2018-10-02 10:50:12 +010033
34/**
35 * Given a process, will iterate over the child threads of the process, and return the CPU usage
36 * statistics for each child thread. The CPU usage statistics contain the amount of time spent in a
37 * frequency band.
Misha Wagner0c5edc362018-10-24 11:28:57 +010038 *
39 * <p>Frequencies are bucketed together to reduce the amount of data created. This means that we
Misha Wagner4b32c9f2019-01-25 15:30:14 +000040 * return less frequencies than provided by {@link ProcTimeInStateReader}. The number of
41 * frequencies is configurable by {@link #setNumBuckets}. Frequencies are reported as the lowest
42 * frequency in that range. Frequencies are spread as evenly as possible across the buckets. The
43 * buckets do not cross over the little/big frequencies reported.
Misha Wagner0c5edc362018-10-24 11:28:57 +010044 *
45 * <p>N.B.: In order to bucket across little/big frequencies correctly, we assume that the {@code
46 * time_in_state} file contains every little core frequency in ascending order, followed by every
47 * big core frequency in ascending order. This assumption might not hold for devices with different
48 * kernel implementations of the {@code time_in_state} file generation.
Misha Wagner566903a2018-10-02 10:50:12 +010049 */
50public class KernelCpuThreadReader {
51
52 private static final String TAG = "KernelCpuThreadReader";
53
54 private static final boolean DEBUG = false;
55
56 /**
57 * The name of the file to read CPU statistics from, must be found in {@code
58 * /proc/$PID/task/$TID}
59 */
60 private static final String CPU_STATISTICS_FILENAME = "time_in_state";
61
62 /**
63 * The name of the file to read process command line invocation from, must be found in
64 * {@code /proc/$PID/}
65 */
66 private static final String PROCESS_NAME_FILENAME = "cmdline";
67
68 /**
69 * The name of the file to read thread name from, must be found in
70 * {@code /proc/$PID/task/$TID}
71 */
72 private static final String THREAD_NAME_FILENAME = "comm";
73
74 /**
Misha Wagnerdc797542018-10-29 13:21:34 +000075 * Glob pattern for the process directory names under {@code proc}
76 */
77 private static final String PROCESS_DIRECTORY_FILTER = "[0-9]*";
78
79 /**
Misha Wagner566903a2018-10-02 10:50:12 +010080 * Default process name when the name can't be read
81 */
82 private static final String DEFAULT_PROCESS_NAME = "unknown_process";
83
84 /**
85 * Default thread name when the name can't be read
86 */
87 private static final String DEFAULT_THREAD_NAME = "unknown_thread";
88
89 /**
90 * Default mount location of the {@code proc} filesystem
91 */
92 private static final Path DEFAULT_PROC_PATH = Paths.get("/proc");
93
94 /**
95 * The initial {@code time_in_state} file for {@link ProcTimeInStateReader}
96 */
97 private static final Path DEFAULT_INITIAL_TIME_IN_STATE_PATH =
98 DEFAULT_PROC_PATH.resolve("self/time_in_state");
99
100 /**
Misha Wagnerdc797542018-10-29 13:21:34 +0000101 * Value returned when there was an error getting an integer ID value (e.g. PID, UID)
102 */
103 private static final int ID_ERROR = -1;
104
105 /**
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000106 * When checking whether to report data for a thread, we check the UID of the thread's owner
107 * against this predicate
108 */
109 private Predicate<Integer> mUidPredicate;
110
111 /**
Misha Wagner648d2032019-02-11 10:43:27 +0000112 * If a thread has strictly less than {@code minimumTotalCpuUsageMillis} total CPU usage, it
113 * will not be reported
114 */
115 private int mMinimumTotalCpuUsageMillis;
116
117 /**
Misha Wagner566903a2018-10-02 10:50:12 +0100118 * Where the proc filesystem is mounted
119 */
120 private final Path mProcPath;
121
122 /**
123 * Frequencies read from the {@code time_in_state} file. Read from {@link
124 * #mProcTimeInStateReader#getCpuFrequenciesKhz()} and cast to {@code int[]}
125 */
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000126 private int[] mFrequenciesKhz;
Misha Wagner566903a2018-10-02 10:50:12 +0100127
128 /**
129 * Used to read and parse {@code time_in_state} files
130 */
131 private final ProcTimeInStateReader mProcTimeInStateReader;
132
Misha Wagner0c5edc362018-10-24 11:28:57 +0100133 /**
134 * Used to sort frequencies and usage times into buckets
135 */
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000136 private FrequencyBucketCreator mFrequencyBucketCreator;
Misha Wagner0c5edc362018-10-24 11:28:57 +0100137
Misha Wagnerdc797542018-10-29 13:21:34 +0000138 private final Injector mInjector;
139
Misha Wagner566903a2018-10-02 10:50:12 +0100140 /**
141 * Create with a path where `proc` is mounted. Used primarily for testing
142 *
143 * @param procPath where `proc` is mounted (to find, see {@code mount | grep ^proc})
144 * @param initialTimeInStatePath where the initial {@code time_in_state} file exists to define
145 * format
146 */
147 @VisibleForTesting
Misha Wagnerdc797542018-10-29 13:21:34 +0000148 public KernelCpuThreadReader(
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000149 int numBuckets,
150 Predicate<Integer> uidPredicate,
Misha Wagner648d2032019-02-11 10:43:27 +0000151 int minimumTotalCpuUsageMillis,
Misha Wagnerdc797542018-10-29 13:21:34 +0000152 Path procPath,
153 Path initialTimeInStatePath,
154 Injector injector) throws IOException {
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000155 mUidPredicate = uidPredicate;
Misha Wagner648d2032019-02-11 10:43:27 +0000156 mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis;
Misha Wagner566903a2018-10-02 10:50:12 +0100157 mProcPath = procPath;
158 mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath);
Misha Wagnerdc797542018-10-29 13:21:34 +0000159 mInjector = injector;
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000160 setNumBuckets(numBuckets);
Misha Wagner566903a2018-10-02 10:50:12 +0100161 }
162
163 /**
164 * Create the reader and handle exceptions during creation
165 *
166 * @return the reader, null if an exception was thrown during creation
167 */
168 @Nullable
Misha Wagner648d2032019-02-11 10:43:27 +0000169 public static KernelCpuThreadReader create(
170 int numBuckets, Predicate<Integer> uidPredicate, int minimumTotalCpuUsageMillis) {
Misha Wagner566903a2018-10-02 10:50:12 +0100171 try {
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000172 return new KernelCpuThreadReader(
173 numBuckets,
174 uidPredicate,
Misha Wagner648d2032019-02-11 10:43:27 +0000175 minimumTotalCpuUsageMillis,
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000176 DEFAULT_PROC_PATH,
177 DEFAULT_INITIAL_TIME_IN_STATE_PATH,
178 new Injector());
Misha Wagner566903a2018-10-02 10:50:12 +0100179 } catch (IOException e) {
180 Slog.e(TAG, "Failed to initialize KernelCpuThreadReader", e);
181 return null;
182 }
183 }
184
185 /**
Misha Wagnerdc797542018-10-29 13:21:34 +0000186 * Get the per-thread CPU usage of all processes belonging to a set of UIDs
187 *
188 * <p>This function will crawl through all process {@code proc} directories found by the pattern
189 * {@code /proc/[0-9]*}, and then check the UID using {@code /proc/$PID/status}. This takes
190 * approximately 500ms on a Pixel 2. Therefore, this method can be computationally expensive,
191 * and should not be called more than once an hour.
192 *
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000193 * <p>Data is only collected for UIDs passing the predicate supplied in {@link
194 * #setUidPredicate}.
Misha Wagnerdc797542018-10-29 13:21:34 +0000195 */
196 @Nullable
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000197 public ArrayList<ProcessCpuUsage> getProcessCpuUsageByUids() {
Misha Wagnerdc797542018-10-29 13:21:34 +0000198 if (DEBUG) {
199 Slog.d(TAG, "Reading CPU thread usages for processes owned by UIDs");
200 }
201
202 final ArrayList<ProcessCpuUsage> processCpuUsages = new ArrayList<>();
203
204 try (DirectoryStream<Path> processPaths =
205 Files.newDirectoryStream(mProcPath, PROCESS_DIRECTORY_FILTER)) {
206 for (Path processPath : processPaths) {
207 final int processId = getProcessId(processPath);
208 final int uid = mInjector.getUidForPid(processId);
209 if (uid == ID_ERROR || processId == ID_ERROR) {
210 continue;
211 }
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000212 if (!mUidPredicate.test(uid)) {
Misha Wagnerdc797542018-10-29 13:21:34 +0000213 continue;
214 }
215
216 final ProcessCpuUsage processCpuUsage =
217 getProcessCpuUsage(processPath, processId, uid);
218 if (processCpuUsage != null) {
219 processCpuUsages.add(processCpuUsage);
220 }
221 }
222 } catch (IOException e) {
223 Slog.w("Failed to iterate over process paths", e);
224 return null;
225 }
226
227 if (processCpuUsages.isEmpty()) {
228 Slog.w(TAG, "Didn't successfully get any process CPU information for UIDs specified");
229 return null;
230 }
231
232 if (DEBUG) {
233 Slog.d(TAG, "Read usage for " + processCpuUsages.size() + " processes");
234 }
235
236 return processCpuUsages;
237 }
238
239 /**
Misha Wagner566903a2018-10-02 10:50:12 +0100240 * Read all of the CPU usage statistics for each child thread of the current process
241 *
242 * @return process CPU usage containing usage of all child threads
243 */
244 @Nullable
245 public ProcessCpuUsage getCurrentProcessCpuUsage() {
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000246 return getProcessCpuUsage(mProcPath.resolve("self"), mInjector.myPid(), mInjector.myUid());
Misha Wagner566903a2018-10-02 10:50:12 +0100247 }
248
249 /**
250 * Read all of the CPU usage statistics for each child thread of a process
251 *
252 * @param processPath the {@code /proc} path of the thread
253 * @param processId the ID of the process
254 * @param uid the ID of the user who owns the process
Misha Wagnerdc797542018-10-29 13:21:34 +0000255 * @return process CPU usage containing usage of all child threads. Null if the process exited
256 * and its {@code proc} directory was removed while collecting information
Misha Wagner566903a2018-10-02 10:50:12 +0100257 */
258 @Nullable
259 private ProcessCpuUsage getProcessCpuUsage(Path processPath, int processId, int uid) {
260 if (DEBUG) {
261 Slog.d(TAG, "Reading CPU thread usages with directory " + processPath
262 + " process ID " + processId
263 + " and user ID " + uid);
264 }
265
266 final Path allThreadsPath = processPath.resolve("task");
267 final ArrayList<ThreadCpuUsage> threadCpuUsages = new ArrayList<>();
268 try (DirectoryStream<Path> threadPaths = Files.newDirectoryStream(allThreadsPath)) {
269 for (Path threadDirectory : threadPaths) {
270 ThreadCpuUsage threadCpuUsage = getThreadCpuUsage(threadDirectory);
271 if (threadCpuUsage != null) {
272 threadCpuUsages.add(threadCpuUsage);
273 }
274 }
275 } catch (IOException e) {
Misha Wagnercf7a07d2018-11-07 12:38:07 +0000276 // Expected when a process finishes
Misha Wagner566903a2018-10-02 10:50:12 +0100277 return null;
278 }
279
280 // If we found no threads, then the process has exited while we were reading from it
281 if (threadCpuUsages.isEmpty()) {
282 return null;
283 }
284
285 if (DEBUG) {
286 Slog.d(TAG, "Read CPU usage of " + threadCpuUsages.size() + " threads");
287 }
288 return new ProcessCpuUsage(
289 processId,
290 getProcessName(processPath),
291 uid,
292 threadCpuUsages);
293 }
294
295 /**
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000296 * Set the number of frequency buckets to use
297 */
298 void setNumBuckets(int numBuckets) {
299 if (numBuckets < 1) {
300 Slog.w(TAG, "Number of buckets must be at least 1, but was " + numBuckets);
301 return;
302 }
303 // If `numBuckets` hasn't changed since the last set, do nothing
304 if (mFrequenciesKhz != null && mFrequenciesKhz.length == numBuckets) {
305 return;
306 }
307 mFrequencyBucketCreator = new FrequencyBucketCreator(
308 mProcTimeInStateReader.getFrequenciesKhz(), numBuckets);
309 mFrequenciesKhz = mFrequencyBucketCreator.getBucketMinFrequencies(
310 mProcTimeInStateReader.getFrequenciesKhz());
311 }
312
313 /**
314 * Set the UID predicate for {@link #getProcessCpuUsageByUids}
315 */
316 void setUidPredicate(Predicate<Integer> uidPredicate) {
317 mUidPredicate = uidPredicate;
318 }
319
320 /**
Misha Wagner648d2032019-02-11 10:43:27 +0000321 * If a thread has strictly less than {@code minimumTotalCpuUsageMillis} total CPU usage, it
322 * will not be reported
323 */
324 void setMinimumTotalCpuUsageMillis(int minimumTotalCpuUsageMillis) {
325 if (minimumTotalCpuUsageMillis < 0) {
326 Slog.w(TAG, "Negative minimumTotalCpuUsageMillis: " + minimumTotalCpuUsageMillis);
327 return;
328 }
329 mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis;
330 }
331
332 /**
Misha Wagner566903a2018-10-02 10:50:12 +0100333 * Get the CPU frequencies that correspond to the times reported in
334 * {@link ThreadCpuUsage#usageTimesMillis}
335 */
336 @Nullable
337 public int[] getCpuFrequenciesKhz() {
338 return mFrequenciesKhz;
339 }
340
341 /**
342 * Get a thread's CPU usage
343 *
344 * @param threadDirectory the {@code /proc} directory of the thread
Misha Wagnerdc797542018-10-29 13:21:34 +0000345 * @return thread CPU usage. Null if the thread exited and its {@code proc} directory was
346 * removed while collecting information
Misha Wagner566903a2018-10-02 10:50:12 +0100347 */
348 @Nullable
349 private ThreadCpuUsage getThreadCpuUsage(Path threadDirectory) {
350 // Get the thread ID from the directory name
351 final int threadId;
352 try {
353 final String directoryName = threadDirectory.getFileName().toString();
354 threadId = Integer.parseInt(directoryName);
355 } catch (NumberFormatException e) {
356 Slog.w(TAG, "Failed to parse thread ID when iterating over /proc/*/task", e);
357 return null;
358 }
359
360 // Get the thread name from the thread directory
361 final String threadName = getThreadName(threadDirectory);
362
363 // Get the CPU statistics from the directory
364 final Path threadCpuStatPath = threadDirectory.resolve(CPU_STATISTICS_FILENAME);
365 final long[] cpuUsagesLong = mProcTimeInStateReader.getUsageTimesMillis(threadCpuStatPath);
366 if (cpuUsagesLong == null) {
367 return null;
368 }
Misha Wagner0c5edc362018-10-24 11:28:57 +0100369 int[] cpuUsages = mFrequencyBucketCreator.getBucketedValues(cpuUsagesLong);
Misha Wagner566903a2018-10-02 10:50:12 +0100370
Misha Wagner648d2032019-02-11 10:43:27 +0000371 // Check if the total CPU usage below the threshold
372 int totalCpuUsage = 0;
373 for (int i = 0; i < cpuUsages.length; i++) {
374 totalCpuUsage += cpuUsages[i];
375 }
376 if (totalCpuUsage < mMinimumTotalCpuUsageMillis) {
377 return null;
378 }
379
Misha Wagner566903a2018-10-02 10:50:12 +0100380 return new ThreadCpuUsage(threadId, threadName, cpuUsages);
381 }
382
383 /**
384 * Get the command used to start a process
385 */
386 private String getProcessName(Path processPath) {
387 final Path processNamePath = processPath.resolve(PROCESS_NAME_FILENAME);
388
389 final String processName =
390 ProcStatsUtil.readSingleLineProcFile(processNamePath.toString());
391 if (processName != null) {
392 return processName;
393 }
394 return DEFAULT_PROCESS_NAME;
395 }
396
397 /**
398 * Get the name of a thread, given the {@code /proc} path of the thread
399 */
400 private String getThreadName(Path threadPath) {
401 final Path threadNamePath = threadPath.resolve(THREAD_NAME_FILENAME);
402 final String threadName =
403 ProcStatsUtil.readNullSeparatedFile(threadNamePath.toString());
404 if (threadName == null) {
405 return DEFAULT_THREAD_NAME;
406 }
407 return threadName;
408 }
409
410 /**
Misha Wagnerdc797542018-10-29 13:21:34 +0000411 * Get the ID of a process from its path
412 *
413 * @param processPath {@code proc} path of the process
414 * @return the ID, {@link #ID_ERROR} if the path could not be parsed
415 */
416 private int getProcessId(Path processPath) {
417 String fileName = processPath.getFileName().toString();
418 try {
419 return Integer.parseInt(fileName);
420 } catch (NumberFormatException e) {
421 Slog.w(TAG, "Failed to parse " + fileName + " as process ID", e);
422 return ID_ERROR;
423 }
424 }
425
426 /**
Misha Wagner0c5edc362018-10-24 11:28:57 +0100427 * Puts frequencies and usage times into buckets
428 */
429 @VisibleForTesting
430 public static class FrequencyBucketCreator {
431 private final int mNumBuckets;
432 private final int mNumFrequencies;
433 private final int mBigFrequenciesStartIndex;
434 private final int mLittleNumBuckets;
435 private final int mBigNumBuckets;
436 private final int mLittleBucketSize;
437 private final int mBigBucketSize;
438
439 /**
440 * Buckets based of a list of frequencies
441 *
442 * @param frequencies the frequencies to base buckets off
443 * @param numBuckets how many buckets to create
444 */
445 @VisibleForTesting
446 public FrequencyBucketCreator(long[] frequencies, int numBuckets) {
447 Preconditions.checkArgument(numBuckets > 0);
448
449 mNumFrequencies = frequencies.length;
450 mBigFrequenciesStartIndex = getBigFrequenciesStartIndex(frequencies);
451
452 final int littleNumBuckets;
453 final int bigNumBuckets;
454 if (mBigFrequenciesStartIndex < frequencies.length) {
455 littleNumBuckets = numBuckets / 2;
456 bigNumBuckets = numBuckets - littleNumBuckets;
457 } else {
458 // If we've got no big frequencies, set all buckets to little frequencies
459 littleNumBuckets = numBuckets;
460 bigNumBuckets = 0;
461 }
462
463 // Ensure that we don't have more buckets than frequencies
464 mLittleNumBuckets = Math.min(littleNumBuckets, mBigFrequenciesStartIndex);
465 mBigNumBuckets = Math.min(
466 bigNumBuckets, frequencies.length - mBigFrequenciesStartIndex);
467 mNumBuckets = mLittleNumBuckets + mBigNumBuckets;
468
469 // Set the size of each little and big bucket. If they have no buckets, the size is zero
470 mLittleBucketSize = mLittleNumBuckets == 0 ? 0 :
471 mBigFrequenciesStartIndex / mLittleNumBuckets;
472 mBigBucketSize = mBigNumBuckets == 0 ? 0 :
473 (frequencies.length - mBigFrequenciesStartIndex) / mBigNumBuckets;
474 }
475
476 /**
477 * Find the index where frequencies change from little core to big core
478 */
479 @VisibleForTesting
480 public static int getBigFrequenciesStartIndex(long[] frequenciesKhz) {
481 for (int i = 0; i < frequenciesKhz.length - 1; i++) {
482 if (frequenciesKhz[i] > frequenciesKhz[i + 1]) {
483 return i + 1;
484 }
485 }
486
487 return frequenciesKhz.length;
488 }
489
490 /**
491 * Get the minimum frequency in each bucket
492 */
493 @VisibleForTesting
494 public int[] getBucketMinFrequencies(long[] frequenciesKhz) {
495 Preconditions.checkArgument(frequenciesKhz.length == mNumFrequencies);
496 // If there's only one bucket, we bucket everything together so the first bucket is the
497 // min frequency
498 if (mNumBuckets == 1) {
499 return new int[]{(int) frequenciesKhz[0]};
500 }
501
502 final int[] bucketMinFrequencies = new int[mNumBuckets];
503 // Initialize little buckets min frequencies
504 for (int i = 0; i < mLittleNumBuckets; i++) {
505 bucketMinFrequencies[i] = (int) frequenciesKhz[i * mLittleBucketSize];
506 }
507 // Initialize big buckets min frequencies
508 for (int i = 0; i < mBigNumBuckets; i++) {
509 final int frequencyIndex = mBigFrequenciesStartIndex + i * mBigBucketSize;
510 bucketMinFrequencies[mLittleNumBuckets + i] = (int) frequenciesKhz[frequencyIndex];
511 }
512 return bucketMinFrequencies;
513 }
514
515 /**
516 * Put an array of values into buckets. This takes a {@code long[]} and returns {@code
517 * int[]} as everywhere this method is used will have to do the conversion anyway, so we
518 * save time by doing it here instead
519 *
520 * @param values the values to bucket
521 * @return the bucketed usage times
522 */
523 @VisibleForTesting
524 public int[] getBucketedValues(long[] values) {
525 Preconditions.checkArgument(values.length == mNumFrequencies);
526 final int[] bucketed = new int[mNumBuckets];
527
528 // If there's only one bucket, add all frequencies in
529 if (mNumBuckets == 1) {
530 for (int i = 0; i < values.length; i++) {
531 bucketed[0] += values[i];
532 }
533 return bucketed;
534 }
535
536 // Initialize the little buckets
537 for (int i = 0; i < mBigFrequenciesStartIndex; i++) {
538 final int bucketIndex = Math.min(i / mLittleBucketSize, mLittleNumBuckets - 1);
539 bucketed[bucketIndex] += values[i];
540 }
541 // Initialize the big buckets
542 for (int i = mBigFrequenciesStartIndex; i < values.length; i++) {
543 final int bucketIndex = Math.min(
544 mLittleNumBuckets + (i - mBigFrequenciesStartIndex) / mBigBucketSize,
545 mNumBuckets - 1);
546 bucketed[bucketIndex] += values[i];
547 }
548 return bucketed;
549 }
550 }
551
552 /**
Misha Wagner566903a2018-10-02 10:50:12 +0100553 * CPU usage of a process
554 */
555 public static class ProcessCpuUsage {
556 public final int processId;
557 public final String processName;
558 public final int uid;
559 public final ArrayList<ThreadCpuUsage> threadCpuUsages;
560
561 ProcessCpuUsage(
562 int processId,
563 String processName,
564 int uid,
565 ArrayList<ThreadCpuUsage> threadCpuUsages) {
566 this.processId = processId;
567 this.processName = processName;
568 this.uid = uid;
569 this.threadCpuUsages = threadCpuUsages;
570 }
571 }
572
573 /**
574 * CPU usage of a thread
575 */
576 public static class ThreadCpuUsage {
577 public final int threadId;
578 public final String threadName;
579 public final int[] usageTimesMillis;
580
581 ThreadCpuUsage(
582 int threadId,
583 String threadName,
584 int[] usageTimesMillis) {
585 this.threadId = threadId;
586 this.threadName = threadName;
587 this.usageTimesMillis = usageTimesMillis;
588 }
589 }
Misha Wagnerdc797542018-10-29 13:21:34 +0000590
591 /**
592 * Used to inject static methods from {@link Process}
593 */
594 @VisibleForTesting
595 public static class Injector {
596 /**
597 * Get the PID of the current process
598 */
599 public int myPid() {
600 return Process.myPid();
601 }
602
603 /**
604 * Get the UID that owns the current process
605 */
606 public int myUid() {
607 return Process.myUid();
608 }
609
610 /**
611 * Get the UID for the process with ID {@code pid}
612 */
613 public int getUidForPid(int pid) {
614 return Process.getUidForPid(pid);
615 }
616 }
Misha Wagner566903a2018-10-02 10:50:12 +0100617}