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