Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 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; |
Misha Wagner | 0c5edc36 | 2018-10-24 11:28:57 +0100 | [diff] [blame] | 24 | import com.android.internal.util.Preconditions; |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 25 | |
| 26 | import java.io.IOException; |
| 27 | import java.nio.file.DirectoryStream; |
| 28 | import java.nio.file.Files; |
| 29 | import java.nio.file.Path; |
| 30 | import java.nio.file.Paths; |
| 31 | import java.util.ArrayList; |
Misha Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 32 | import java.util.function.Predicate; |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 33 | |
| 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 Wagner | 0c5edc36 | 2018-10-24 11:28:57 +0100 | [diff] [blame] | 38 | * |
| 39 | * <p>Frequencies are bucketed together to reduce the amount of data created. This means that we |
Misha Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 40 | * 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 Wagner | 0c5edc36 | 2018-10-24 11:28:57 +0100 | [diff] [blame] | 44 | * |
| 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 Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 49 | */ |
| 50 | public 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 Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 75 | * Glob pattern for the process directory names under {@code proc} |
| 76 | */ |
| 77 | private static final String PROCESS_DIRECTORY_FILTER = "[0-9]*"; |
| 78 | |
| 79 | /** |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 80 | * 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 Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 101 | * 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 Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 106 | * 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 Wagner | 648d203 | 2019-02-11 10:43:27 +0000 | [diff] [blame] | 112 | * 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 Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 118 | * 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 Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 126 | private int[] mFrequenciesKhz; |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 127 | |
| 128 | /** |
| 129 | * Used to read and parse {@code time_in_state} files |
| 130 | */ |
| 131 | private final ProcTimeInStateReader mProcTimeInStateReader; |
| 132 | |
Misha Wagner | 0c5edc36 | 2018-10-24 11:28:57 +0100 | [diff] [blame] | 133 | /** |
| 134 | * Used to sort frequencies and usage times into buckets |
| 135 | */ |
Misha Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 136 | private FrequencyBucketCreator mFrequencyBucketCreator; |
Misha Wagner | 0c5edc36 | 2018-10-24 11:28:57 +0100 | [diff] [blame] | 137 | |
Misha Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 138 | private final Injector mInjector; |
| 139 | |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 140 | /** |
| 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 Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 148 | public KernelCpuThreadReader( |
Misha Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 149 | int numBuckets, |
| 150 | Predicate<Integer> uidPredicate, |
Misha Wagner | 648d203 | 2019-02-11 10:43:27 +0000 | [diff] [blame] | 151 | int minimumTotalCpuUsageMillis, |
Misha Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 152 | Path procPath, |
| 153 | Path initialTimeInStatePath, |
| 154 | Injector injector) throws IOException { |
Misha Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 155 | mUidPredicate = uidPredicate; |
Misha Wagner | 648d203 | 2019-02-11 10:43:27 +0000 | [diff] [blame] | 156 | mMinimumTotalCpuUsageMillis = minimumTotalCpuUsageMillis; |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 157 | mProcPath = procPath; |
| 158 | mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath); |
Misha Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 159 | mInjector = injector; |
Misha Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 160 | setNumBuckets(numBuckets); |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 161 | } |
| 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 Wagner | 648d203 | 2019-02-11 10:43:27 +0000 | [diff] [blame] | 169 | public static KernelCpuThreadReader create( |
| 170 | int numBuckets, Predicate<Integer> uidPredicate, int minimumTotalCpuUsageMillis) { |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 171 | try { |
Misha Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 172 | return new KernelCpuThreadReader( |
| 173 | numBuckets, |
| 174 | uidPredicate, |
Misha Wagner | 648d203 | 2019-02-11 10:43:27 +0000 | [diff] [blame] | 175 | minimumTotalCpuUsageMillis, |
Misha Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 176 | DEFAULT_PROC_PATH, |
| 177 | DEFAULT_INITIAL_TIME_IN_STATE_PATH, |
| 178 | new Injector()); |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 179 | } catch (IOException e) { |
| 180 | Slog.e(TAG, "Failed to initialize KernelCpuThreadReader", e); |
| 181 | return null; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | /** |
Misha Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 186 | * 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 Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 193 | * <p>Data is only collected for UIDs passing the predicate supplied in {@link |
| 194 | * #setUidPredicate}. |
Misha Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 195 | */ |
| 196 | @Nullable |
Misha Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 197 | public ArrayList<ProcessCpuUsage> getProcessCpuUsageByUids() { |
Misha Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 198 | 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 Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 212 | if (!mUidPredicate.test(uid)) { |
Misha Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 213 | 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 Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 240 | * 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 Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 246 | return getProcessCpuUsage(mProcPath.resolve("self"), mInjector.myPid(), mInjector.myUid()); |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 247 | } |
| 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 Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 255 | * @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 Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 257 | */ |
| 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 Wagner | cf7a07d | 2018-11-07 12:38:07 +0000 | [diff] [blame] | 276 | // Expected when a process finishes |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 277 | 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 Wagner | 4b32c9f | 2019-01-25 15:30:14 +0000 | [diff] [blame] | 296 | * 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 Wagner | 648d203 | 2019-02-11 10:43:27 +0000 | [diff] [blame] | 321 | * 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 Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 333 | * 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 Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 345 | * @return thread CPU usage. Null if the thread exited and its {@code proc} directory was |
| 346 | * removed while collecting information |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 347 | */ |
| 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 Wagner | 0c5edc36 | 2018-10-24 11:28:57 +0100 | [diff] [blame] | 369 | int[] cpuUsages = mFrequencyBucketCreator.getBucketedValues(cpuUsagesLong); |
Misha Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 370 | |
Misha Wagner | 648d203 | 2019-02-11 10:43:27 +0000 | [diff] [blame] | 371 | // 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 Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 380 | 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 Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 411 | * 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 Wagner | 0c5edc36 | 2018-10-24 11:28:57 +0100 | [diff] [blame] | 427 | * 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 Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 553 | * 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 Wagner | dc79754 | 2018-10-29 13:21:34 +0000 | [diff] [blame] | 590 | |
| 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 Wagner | 566903a | 2018-10-02 10:50:12 +0100 | [diff] [blame] | 617 | } |