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 if (intervalRate > 0L) { 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 from a stream representing the output of `dumpsys graphicsstats` 197 * 198 * This is broken out from gatherGraphicsStats for testing purposes 199 */ parseGraphicsStatsFromStream(BufferedReader stream)200 private List<JankStat> parseGraphicsStatsFromStream(BufferedReader stream) throws IOException { 201 // TODO: this kind of stream filtering is much nicer using the Java 8 functional 202 // primitives. Once AUPT goes to jdk8, we should refactor this. 203 204 JankStat.StatPattern patterns[] = JankStat.StatPattern.values(); 205 List<JankStat> result = new ArrayList<>(); 206 JankStat nextStat = new JankStat(null, 0L, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); 207 String line; 208 209 // Split the stream by package 210 while((line = stream.readLine()) != null) { 211 if(JankStat.StatPattern.PACKAGE.parse(line) != null) { 212 213 // When the output of `dumpsys graphicsstats` enters a new set of jank stats, we start 214 // with a line matching the PACKAGE pattern; so we have to make a new JankStat for it and 215 // save the old one. 216 217 if(nextStat.packageName != null) { 218 Log.v(TAG, String.format("Gathered jank info from process %s.", nextStat.packageName)); 219 result.add(nextStat); 220 } 221 222 nextStat = new JankStat(JankStat.StatPattern.PACKAGE.parse(line), 223 0L, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); 224 225 } else { 226 227 // NOTE: we know these theoretically come in order, so we don't have to iterate 228 // through the whole pattern array every time; but there is enough variation in 229 // the code generating these log lines to justify a more input-robust parser 230 231 for (JankStat.StatPattern p : patterns) { 232 if (p.getPattern() != null && p.parse(line) != null) { 233 switch (p) { 234 case STATS_SINCE: 235 nextStat.statsSince = Long.parseLong(p.parse(line)); 236 break; 237 case TOTAL_FRAMES: 238 nextStat.totalFrames = Integer.valueOf(p.parse(line)); 239 break; 240 case NUM_JANKY: 241 nextStat.jankyFrames = Integer.valueOf(p.parse(line)); 242 break; 243 case FRAME_TIME_50TH: 244 nextStat.frameTime50th = Integer.valueOf(p.parse(line)); 245 break; 246 case FRAME_TIME_90TH: 247 nextStat.frameTime90th = Integer.valueOf(p.parse(line)); 248 break; 249 case FRAME_TIME_95TH: 250 nextStat.frameTime95th = Integer.valueOf(p.parse(line)); 251 break; 252 case FRAME_TIME_99TH: 253 nextStat.frameTime99th = Integer.valueOf(p.parse(line)); 254 break; 255 case NUM_MISSED_VSYNC: 256 nextStat.numMissedVsync = Integer.valueOf(p.parse(line)); 257 break; 258 case NUM_HIGH_INPUT_LATENCY: 259 nextStat.numHighLatency = Integer.valueOf(p.parse(line)); 260 break; 261 case NUM_SLOW_UI_THREAD: 262 nextStat.numSlowUiThread = Integer.valueOf(p.parse(line)); 263 break; 264 case NUM_SLOW_BITMAP_UPLOADS: 265 nextStat.numSlowBitmap = Integer.valueOf(p.parse(line)); 266 break; 267 case NUM_SLOW_DRAW: 268 nextStat.numSlowDraw = Integer.valueOf(p.parse(line)); 269 break; 270 default: 271 throw new RuntimeException( 272 "Unexpected parsing state in GraphicsStateMonitor"); 273 } 274 } 275 } 276 } 277 } 278 279 // Remember to add the last JankStat 280 // We can't wrap this in the previous call because BufferedReader doesn't have a .peek() 281 if(nextStat.packageName != null) { 282 Log.v(TAG, String.format("Gathered jank info from process %s.", nextStat.packageName)); 283 result.add(nextStat); 284 } 285 286 return result; 287 } 288 289 /** 290 * Return JankStat objects with metric data for all currently tracked processes 291 */ gatherGraphicsStats()292 public List<JankStat> gatherGraphicsStats () { 293 Log.v(TAG, "Gather all graphics stats"); 294 BufferedReader stream = executeShellCommand("dumpsys graphicsstats"); 295 296 try { 297 return parseGraphicsStatsFromStream(stream); 298 } catch (IOException exception) { 299 Log.e(TAG, "Error with buffered reader", exception); 300 return null; 301 } finally { 302 try { 303 if (stream != null) { 304 stream.close(); 305 } 306 } catch (IOException exception) { 307 Log.e(TAG, "Error with closing the stream", exception); 308 } 309 } 310 } 311 312 /** 313 * UiAutomation is included solely for the purpose of executing shell commands 314 */ 315 private UiAutomation mUiAutomation; 316 317 /** 318 * Executes a shell command through UiAutomation and puts the results in an 319 * InputStreamReader that is returned inside a BufferedReader. 320 * @param command the command to be executed in the adb shell 321 * @result a BufferedReader that reads the command output 322 */ executeShellCommand(String command)323 public BufferedReader executeShellCommand (String command) { 324 ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command); 325 326 BufferedReader stream = new BufferedReader(new InputStreamReader( 327 new ParcelFileDescriptor.AutoCloseInputStream(stdout))); 328 return stream; 329 } 330 331 /** 332 * Sets the UiAutomation member for shell execution 333 */ setUiAutomation(UiAutomation uiAutomation)334 public void setUiAutomation (UiAutomation uiAutomation) { 335 mUiAutomation = uiAutomation; 336 } 337 338 /** 339 * @return UiAutomation instance from Aupt instrumentation 340 */ getUiAutomation()341 public UiAutomation getUiAutomation () { 342 return mUiAutomation; 343 } 344 } 345