1 /* 2 * Copyright (C) 2015 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 android.support.test.aupt; 18 19 import android.app.UiAutomation; 20 import android.os.Bundle; 21 import android.os.ParcelFileDescriptor; 22 import android.util.Log; 23 24 import java.io.BufferedReader; 25 import java.io.InputStreamReader; 26 import java.io.IOException; 27 import java.util.ArrayList; 28 import java.util.HashMap; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.Timer; 32 import java.util.TimerTask; 33 34 /** 35 * GraphicsStatsMonitor is an internal monitor for AUPT to poll and track the information coming out 36 * of the shell command, "dumpsys graphicsstats." In particular, the purpose of this is to see jank 37 * statistics across the lengthy duration of an AUPT run. 38 * <p> 39 * To use the monitor, simply specify the options: trackJank true and jankInterval n, where n is 40 * the polling interval in milliseconds. The default is 5 minutes. Also, it should be noted that 41 * the trackJank option is unnecessary and this comment should be removed at the same time as it. 42 * <p> 43 * This graphics service monitors jank levels grouped by foreground process. Even when the process 44 * is killed, the monitor will continue to track information, unless the buffer runs out of space. 45 * This should only occur when too many foreground processes have been killed and the service 46 * decides to clear itself. When pulling the information out of the monitor, these separate images 47 * are combined to provide a single image as output. The linear information is preserved by simply 48 * adding the values together. However, certain information such as the jank percentiles are 49 * approximated using a weighted average. 50 */ 51 public class GraphicsStatsMonitor { 52 private static final String TAG = "GraphicsStatsMonitor"; 53 54 public static final int MS_IN_SECS = 1000; 55 public static final int SECS_IN_MIN = 60; 56 public static final long DEFAULT_INTERVAL_RATE = 5 * SECS_IN_MIN * MS_IN_SECS; 57 58 private Timer mIntervalTimer; 59 private TimerTask mIntervalTask; 60 private long mIntervalRate; 61 private boolean mIsRunning; 62 63 private Map<String, List<JankStat>> mGraphicsStatsRecords; 64 65 GraphicsStatsMonitor()66 public GraphicsStatsMonitor () { 67 mIntervalTask = new TimerTask() { 68 @Override 69 public void run () { 70 if (mIsRunning) { 71 grabStatsImage(); 72 } 73 } 74 }; 75 mIntervalRate = DEFAULT_INTERVAL_RATE; 76 mIsRunning = false; 77 } 78 79 /** 80 * Sets the monitoring interval rate if the monitor isn't currently running 81 */ setIntervalRate(long intervalRate)82 public void setIntervalRate (long intervalRate) { 83 if (mIsRunning) { 84 Log.e(TAG, "Can't set interval rate for monitor that is already running"); 85 } else { 86 mIntervalRate = intervalRate; 87 Log.v(TAG, String.format("Set jank monitor interval rate to %d", intervalRate)); 88 } 89 } 90 91 /** 92 * Starts to monitor graphics stats on the interval timer after clearing the stats 93 */ startMonitoring()94 public void startMonitoring () { 95 if (mGraphicsStatsRecords == null) { 96 mGraphicsStatsRecords = new HashMap<>(); 97 } 98 99 clearGraphicsStats(); 100 101 // Schedule a daemon timer to grab stats periodically 102 mIntervalTimer = new Timer(true); 103 mIntervalTimer.schedule(mIntervalTask, 0, mIntervalRate); 104 105 mIsRunning = true; 106 Log.d(TAG, "Started monitoring graphics stats"); 107 } 108 109 /** 110 * Stops monitoring graphics stats by canceling the interval timer 111 */ stopMonitoring()112 public void stopMonitoring () { 113 mIntervalTimer.cancel(); 114 115 mIsRunning = false; 116 Log.d(TAG, "Stopped monitoring graphics stats"); 117 } 118 119 /** 120 * Takes a snapshot of the graphics stats and incorporates them into the process stats history 121 */ grabStatsImage()122 public void grabStatsImage () { 123 Log.v(TAG, "Grabbing image of graphics stats"); 124 List<JankStat> allStats = gatherGraphicsStats(); 125 126 for (JankStat procStats : allStats) { 127 List<JankStat> history; 128 if (mGraphicsStatsRecords.containsKey(procStats.packageName)) { 129 history = mGraphicsStatsRecords.get(procStats.packageName); 130 // Has the process been killed and restarted? 131 if (procStats.isContinuedFrom(history.get(history.size() - 1))) { 132 // Process hasn't been killed and restarted; put the data 133 history.set(history.size() - 1, procStats); 134 Log.v(TAG, String.format("Process %s stats have not changed, overwriting data.", 135 procStats.packageName)); 136 } else { 137 // Process has been killed and restarted; append the data 138 history.add(procStats); 139 Log.v(TAG, String.format("Process %s stats were restarted, appending data.", 140 procStats.packageName)); 141 } 142 } else { 143 // Initialize the process stats history list 144 history = new ArrayList<>(); 145 history.add(procStats); 146 // Put the history list in the JankStats map 147 mGraphicsStatsRecords.put(procStats.packageName, history); 148 Log.v(TAG, String.format("New process, %s. Creating jank history.", 149 procStats.packageName)); 150 } 151 } 152 } 153 154 /** 155 * Aggregates the graphics stats for each process over its history. Merging specifications can 156 * be found in the static method {@link JankStat#mergeStatHistory}. 157 */ aggregateStatsImages()158 public List<JankStat> aggregateStatsImages () { 159 Log.d(TAG, "Aggregating graphics stats history"); 160 List<JankStat> mergedStatsList = new ArrayList<JankStat>(); 161 162 for (Map.Entry<String, List<JankStat>> record : mGraphicsStatsRecords.entrySet()) { 163 String proc = record.getKey(); 164 List<JankStat> history = record.getValue(); 165 166 Log.v(TAG, String.format("Aggregating stats for %s (%d set%s)", proc, history.size(), 167 (history.size() > 1 ? "s" : ""))); 168 169 JankStat mergedStats = JankStat.mergeStatHistory(history); 170 mergedStatsList.add(mergedStats); 171 } 172 173 return mergedStatsList; 174 } 175 176 /** 177 * Clears all graphics stats history data for all processes 178 */ clearStatsImages()179 public void clearStatsImages () { 180 mGraphicsStatsRecords.clear(); 181 } 182 183 /** 184 * Resets graphics stats for all currently tracked processes 185 */ clearGraphicsStats()186 public void clearGraphicsStats () { 187 Log.d(TAG, "Reset all graphics stats"); 188 List<JankStat> existingStats = gatherGraphicsStats(); 189 for (JankStat stat : existingStats) { 190 executeShellCommand(String.format("dumpsys gfxinfo %s reset", stat.packageName)); 191 Log.v(TAG, String.format("Cleared graphics stats for %s", stat.packageName)); 192 } 193 } 194 195 /** 196 * Return JankStat objects with metric data for all currently tracked processes 197 */ gatherGraphicsStats()198 public List<JankStat> gatherGraphicsStats () { 199 Log.v(TAG, "Gather all graphics stats"); 200 List<JankStat> result = new ArrayList<>(); 201 202 BufferedReader stream = executeShellCommand("dumpsys graphicsstats"); 203 try { 204 // Read the stream process by process 205 String line; 206 207 while ((line = stream.readLine()) != null) { 208 String proc = JankStat.StatPattern.PACKAGE.parse(line); 209 if (proc != null) { 210 // "Package: a.b.c" 211 Log.v(TAG, String.format("Found process, %s. Gathering jank info.", proc)); 212 // "Stats since: ###ns" 213 line = stream.readLine(); 214 Long since = Long.parseLong(JankStat.StatPattern.STATS_SINCE.parse(line)); 215 // "Total frames rendered: ###" 216 line = stream.readLine(); 217 int total = Integer.valueOf(JankStat.StatPattern.TOTAL_FRAMES.parse(line)); 218 // "Janky frames: ## (#.#%)" OR 219 // "Janky frames: ## (nan%)" 220 line = stream.readLine(); 221 int janky = Integer.valueOf(JankStat.StatPattern.NUM_JANKY.parse(line)); 222 // (optional, N+) "50th percentile: ##ms" 223 line = stream.readLine(); 224 int perc50; 225 String parsed50 = JankStat.StatPattern.FRAME_TIME_50TH.parse(line); 226 if (parsed50 != null || !parsed50.isEmpty()) { 227 perc50 = Integer.valueOf(parsed50); 228 line = stream.readLine(); 229 } else { 230 perc50 = -1; 231 } 232 // "90th percentile: ##ms" 233 int perc90 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_90TH.parse(line)); 234 // "95th percentile: ##ms" 235 line = stream.readLine(); 236 int perc95 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_95TH.parse(line)); 237 // "99th percentile: ##ms" 238 line = stream.readLine(); 239 int perc99 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_99TH.parse(line)); 240 // "Slowest frames last 24h: ##ms ##ms ..." 241 line = stream.readLine(); 242 String slowest = JankStat.StatPattern.SLOWEST_FRAMES_24H.parse(line); 243 if (slowest != null && !slowest.isEmpty()) { 244 line = stream.readLine(); 245 } 246 // "Number Missed Vsync: #" 247 int vsync = Integer.valueOf(JankStat.StatPattern.NUM_MISSED_VSYNC.parse(line)); 248 // "Number High input latency: #" 249 line = stream.readLine(); 250 int latency = Integer.valueOf( 251 JankStat.StatPattern.NUM_HIGH_INPUT_LATENCY.parse(line)); 252 // "Number slow UI thread: #" 253 line = stream.readLine(); 254 int ui = Integer.valueOf(JankStat.StatPattern.NUM_SLOW_UI_THREAD.parse(line)); 255 // "Number Slow bitmap uploads: #" 256 line = stream.readLine(); 257 int bmp = Integer.valueOf( 258 JankStat.StatPattern.NUM_SLOW_BITMAP_UPLOADS.parse(line)); 259 // "Number slow issue draw commands: #" 260 line = stream.readLine(); 261 int draw = Integer.valueOf(JankStat.StatPattern.NUM_SLOW_DRAW.parse(line)); 262 263 JankStat stat = new JankStat(proc, since, total, janky, perc50, perc90, perc95, 264 perc99, slowest, vsync, latency, ui, bmp, draw, 1); 265 result.add(stat); 266 } 267 } 268 269 return result; 270 } catch (IOException exception) { 271 Log.e(TAG, "Error with buffered reader", exception); 272 return null; 273 } finally { 274 try { 275 if (stream != null) { 276 stream.close(); 277 } 278 } catch (IOException exception) { 279 Log.e(TAG, "Error with closing the stream", exception); 280 } 281 } 282 } 283 284 /** 285 * UiAutomation is included solely for the purpose of executing shell commands 286 */ 287 private UiAutomation mUiAutomation; 288 289 /** 290 * Executes a shell command through UiAutomation and puts the results in an 291 * InputStreamReader that is returned inside a BufferedReader. 292 * @param command the command to be executed in the adb shell 293 * @result a BufferedReader that reads the command output 294 */ executeShellCommand(String command)295 public BufferedReader executeShellCommand (String command) { 296 ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command); 297 298 BufferedReader stream = new BufferedReader(new InputStreamReader( 299 new ParcelFileDescriptor.AutoCloseInputStream(stdout))); 300 return stream; 301 } 302 303 /** 304 * Sets the UiAutomation member for shell execution 305 */ setUiAutomation(UiAutomation uiAutomation)306 public void setUiAutomation (UiAutomation uiAutomation) { 307 mUiAutomation = uiAutomation; 308 } 309 310 /** 311 * @return UiAutomation instance from Aupt instrumentation 312 */ getUiAutomation()313 public UiAutomation getUiAutomation () { 314 return mUiAutomation; 315 } 316 } 317