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