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