• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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