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