blob: 3d89a28ea461f7c683f64195a532e6ef149ffb37 [file] [log] [blame]
Chris Craikb122baf2015-03-05 13:58:42 -08001<!DOCTYPE html>
2<!--
3Copyright (c) 2013 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
Chris Craik44c28202015-05-12 17:25:16 -07007<link rel="import" href="/base/iteration_helpers.html">
Chris Craikbeca7ae2015-04-07 13:29:55 -07008<link rel="import" href="/base/statistics.html">
Chris Craik44c28202015-05-12 17:25:16 -07009<link rel="import" href="/base/time.html">
Chris Craikb122baf2015-03-05 13:58:42 -080010<link rel="import" href="/core/auditor.html">
Chris Craikbeca7ae2015-04-07 13:29:55 -070011<link rel="import" href="/core/trace_model/alert.html">
12<link rel="import" href="/core/trace_model/frame.html">
13<link rel="import" href="/core/trace_model/interaction_record.html">
Chris Craikb122baf2015-03-05 13:58:42 -080014<link rel="import" href="/extras/audits/android_model_helper.html">
15<link rel="import" href="/extras/audits/utils.html">
16
17<script>
18'use strict';
19
20/**
21 * @fileoverview Class for Android-specific Auditing
22 */
23tv.exportTo('tv.e.audits', function() {
Chris Craik44c28202015-05-12 17:25:16 -070024 var SCHEDULING_STATE = tv.c.trace_model.SCHEDULING_STATE;
Chris Craikbeca7ae2015-04-07 13:29:55 -070025 var Auditor = tv.c.Auditor;
26 var AndroidModelHelper = tv.e.audits.AndroidModelHelper;
27 var Statistics = tv.b.Statistics;
28 var FRAME_PERF_CLASS = tv.c.trace_model.FRAME_PERF_CLASS;
Chris Craikbeca7ae2015-04-07 13:29:55 -070029 var InteractionRecord = tv.c.trace_model.InteractionRecord;
Chris Craikbeca7ae2015-04-07 13:29:55 -070030 var Alert = tv.c.trace_model.Alert;
Chris Craik19832152015-04-16 15:43:38 -070031 var EventInfo = tv.c.trace_model.EventInfo;
Chris Craik44c28202015-05-12 17:25:16 -070032 var TimeDuration = tv.b.TimeDuration;
Chris Craikbeca7ae2015-04-07 13:29:55 -070033
Chris Craikb122baf2015-03-05 13:58:42 -080034 // TODO: extract from VSYNC, since not all devices have vsync near 60fps
35 var EXPECTED_FRAME_TIME_MS = 16.67;
36
Chris Craikbeca7ae2015-04-07 13:29:55 -070037 function getStart(e) { return e.start; }
38 function getDuration(e) { return e.duration; }
Chris Craik44c28202015-05-12 17:25:16 -070039 // used for general UI thread responsiveness alerts, falls back to duration
40 function getCpuDuration(e) {
41 return (e.cpuDuration !== undefined) ? e.cpuDuration : e.duration;
42 }
Chris Craikbeca7ae2015-04-07 13:29:55 -070043
44 function frameIsActivityStart(frame) {
Chris Craik44c28202015-05-12 17:25:16 -070045 for (var i = 0; i < frame.associatedEvents.length; i++) {
46 if (frame.associatedEvents[i].title == 'activityStart')
Chris Craikbeca7ae2015-04-07 13:29:55 -070047 return true;
48 }
49 return false;
50 }
51 // TODO: use sane values or different metric
52 var MAX_TIME_UNSCHEDULED = 3.0;
53 var MAX_TIME_BLOCKING_IO = 5.0;
54
Chris Craikb122baf2015-03-05 13:58:42 -080055 var Auditor = tv.c.Auditor;
56 var AndroidModelHelper = tv.e.audits.AndroidModelHelper;
57
Chris Craik19832152015-04-16 15:43:38 -070058 function frameMissedDeadline(frame) {
59 return frame.args['deadline'] && frame.args['deadline'] < frame.end;
60 }
61
Chris Craikb122baf2015-03-05 13:58:42 -080062 /**
63 * Auditor for Android-specific traces.
64 * @constructor
65 */
66 function AndroidAuditor(model) {
67 this.model = model;
68 var helper = new AndroidModelHelper(model);
69 if (helper.apps.length || helper.surfaceFlinger)
70 this.helper = helper;
71 };
72
Chris Craikbeca7ae2015-04-07 13:29:55 -070073 //////////////////////////////////////////////////////////////////////////////
74 // Rendering / RenderThread alerts - only available on SDK 22+
75 //////////////////////////////////////////////////////////////////////////////
76
Chris Craik19832152015-04-16 15:43:38 -070077 AndroidAuditor.viewAlphaAlertInfo_ = new EventInfo(
78 'Inefficient View alpha usage',
79 'Setting an alpha between 0 and 1 has significant performance costs, if one of the fast alpha paths is not used.', // @suppress longLineCheck
80 'http://developer.android.com/reference/android/view/View.html#setAlpha(float)'); // @suppress longLineCheck
81 AndroidAuditor.saveLayerAlertInfo_ = new EventInfo(
82 'Expensive rendering with Canvas#saveLayer()',
83 'Canvas#saveLayer() incurs extremely high rendering cost. They disrupt the rendering pipeline when drawn, forcing a flush of drawing content. Instead use View hardware layers, or static Bitmaps. This enables the offscreen buffers to be reused in between frames, and avoids the disruptive render target switch.', // @suppress longLineCheck
84 'https://developer.android.com/reference/android/graphics/Canvas.html#saveLayerAlpha(android.graphics.RectF, int, int)'); // @suppress longLineCheck
85 AndroidAuditor.getSaveLayerAlerts_ = function(frame) {
86 var badAlphaRegEx =
Chris Craikbeca7ae2015-04-07 13:29:55 -070087 /^(.+) alpha caused (unclipped )?saveLayer (\d+)x(\d+)$/;
Chris Craik19832152015-04-16 15:43:38 -070088 var saveLayerRegEx = /^(unclipped )?saveLayer (\d+)x(\d+)$/;
89
90 var ret = [];
91 var events = [];
92
Chris Craik44c28202015-05-12 17:25:16 -070093 frame.associatedEvents.forEach(function(slice) {
Chris Craik19832152015-04-16 15:43:38 -070094 var match = badAlphaRegEx.exec(slice.title);
Chris Craikbeca7ae2015-04-07 13:29:55 -070095 if (match) {
Chris Craik19832152015-04-16 15:43:38 -070096 // due to bug in tracing code on SDK 22, ignore
97 // presence of 'unclipped' string in View alpha slices
Chris Craikbeca7ae2015-04-07 13:29:55 -070098 var args = { 'view name': match[1],
99 width: parseInt(match[3]),
100 height: parseInt(match[4]) };
Chris Craik19832152015-04-16 15:43:38 -0700101 ret.push(new Alert(AndroidAuditor.viewAlphaAlertInfo_,
102 slice.start, [slice], args));
103 } else if (saveLayerRegEx.test(slice.title))
104 events.push(slice);
Chris Craikbeca7ae2015-04-07 13:29:55 -0700105 }, this);
Chris Craik19832152015-04-16 15:43:38 -0700106
107 if (events.length > ret.length) {
108 // more saveLayers than bad alpha can account for - add another alert
109
110 var unclippedSeen = Statistics.sum(events, function(slice) {
111 return saveLayerRegEx.exec(slice.title)[1] ? 1 : 0;
112 });
113 var clippedSeen = events.length - unclippedSeen;
114 var earliestStart = Statistics.min(events, function(slice) {
115 return slice.start;
116 });
117
118 var args = {
Chris Craik44c28202015-05-12 17:25:16 -0700119 'Unclipped saveLayer count (especially bad!)': unclippedSeen,
120 'Clipped saveLayer count': clippedSeen
Chris Craik19832152015-04-16 15:43:38 -0700121 };
122
123 events.push(frame);
124 ret.push(new Alert(AndroidAuditor.saveLayerAlertInfo_,
125 earliestStart, events, args));
126 }
127
Chris Craikbeca7ae2015-04-07 13:29:55 -0700128 return ret;
Chris Craik19832152015-04-16 15:43:38 -0700129 };
130
131
132 AndroidAuditor.pathAlertInfo_ = new EventInfo(
133 'Path texture churn',
134 'Paths are drawn with a mask texture, so when a path is modified / newly drawn, that texture must be generated and uploaded to the GPU. Ensure that you cache paths between frames and do not unnecessarily call Path#reset(). You can cut down on this cost by sharing Path object instances between drawables/views.'); // @suppress longLineCheck
135 AndroidAuditor.getPathAlert_ = function(frame) {
136 var uploadRegEx = /^Generate Path Texture$/;
137
Chris Craik44c28202015-05-12 17:25:16 -0700138 var events = frame.associatedEvents.filter(function(event) {
Chris Craik19832152015-04-16 15:43:38 -0700139 return event.title == 'Generate Path Texture';
140 });
141 var start = Statistics.min(events, getStart);
142 var duration = Statistics.sum(events, getDuration);
143
144 if (duration < 3)
145 return undefined;
146
147 events.push(frame);
148 return new Alert(AndroidAuditor.pathAlertInfo_, start, events,
Chris Craik44c28202015-05-12 17:25:16 -0700149 { 'Time spent': new TimeDuration(duration) });
Chris Craikbeca7ae2015-04-07 13:29:55 -0700150 }
151
Chris Craik19832152015-04-16 15:43:38 -0700152
153 AndroidAuditor.uploadAlertInfo_ = new EventInfo(
154 'Expensive Bitmap uploads',
155 'Bitmaps that have been modified / newly drawn must be uploaded to the GPU. Since this is expensive if the total number of pixels uploaded is large, reduce the amount of Bitmap churn in this animation/context, per frame.'); // @suppress longLineCheck
Chris Craikbeca7ae2015-04-07 13:29:55 -0700156 AndroidAuditor.getUploadAlert_ = function(frame) {
157 var uploadRegEx = /^Upload (\d+)x(\d+) Texture$/;
158
159 var events = [];
160 var start = Number.POSITIVE_INFINITY;
161 var duration = 0;
162 var pixelsUploaded = 0;
Chris Craik44c28202015-05-12 17:25:16 -0700163 frame.associatedEvents.forEach(function(event) {
Chris Craikbeca7ae2015-04-07 13:29:55 -0700164 var match = uploadRegEx.exec(event.title);
165 if (match) {
166 events.push(event);
167 start = Math.min(start, event.start);
168 duration += event.duration;
169 pixelsUploaded += parseInt(match[1]) * parseInt(match[2]);
170 }
171 });
172 if (events.length == 0 || duration < 3)
173 return undefined;
174
Chris Craik44c28202015-05-12 17:25:16 -0700175 var mPixels = (pixelsUploaded / 1000000).toFixed(2) + ' million';
176 var args = { 'Pixels uploaded': mPixels,
177 'Time spent': new TimeDuration(duration) };
Chris Craik19832152015-04-16 15:43:38 -0700178 events.push(frame);
179 return new Alert(AndroidAuditor.uploadAlertInfo_, start, events, args);
Chris Craikbeca7ae2015-04-07 13:29:55 -0700180 }
181
182 //////////////////////////////////////////////////////////////////////////////
183 // UI responsiveness alerts
184 //////////////////////////////////////////////////////////////////////////////
185
Chris Craik44c28202015-05-12 17:25:16 -0700186 AndroidAuditor.ListViewInflateAlertInfo_ = new EventInfo(
187 'Inflation during ListView recycling',
188 'ListView item recycling involved inflating views. Ensure your Adapter#getView() recycles the incoming View, instead of constructing a new one.'); // @suppress longLineCheck
189 AndroidAuditor.ListViewBindAlertInfo_ = new EventInfo(
190 'Inefficient ListView recycling/rebinding',
191 'ListView recycling taking too much time per frame. Ensure your Adapter#getView() binds data efficiently.'); // @suppress longLineCheck
Chris Craikbeca7ae2015-04-07 13:29:55 -0700192 AndroidAuditor.getListViewAlert_ = function(frame) {
Chris Craik44c28202015-05-12 17:25:16 -0700193 var events = frame.associatedEvents.filter(function(event) {
Chris Craikbeca7ae2015-04-07 13:29:55 -0700194 return event.title == 'obtainView' || event.title == 'setupListItem';
195 });
Chris Craik44c28202015-05-12 17:25:16 -0700196 var duration = Statistics.sum(events, getCpuDuration);
Chris Craikbeca7ae2015-04-07 13:29:55 -0700197
198 if (events.length == 0 || duration < 3)
199 return undefined;
200
Chris Craik44c28202015-05-12 17:25:16 -0700201 // simplifying assumption - check for *any* inflation.
202 // TODO(ccraik): make 'inflate' slices associated events.
203 var hasInflation = false;
204 for (var i = 0; i < events.length; i++) {
205 if (events[i] instanceof tv.c.trace_model.Slice &&
206 events[i].findDescendentSlice('inflate')) {
207 hasInflation = true;
208 break;
209 }
210 }
211
Chris Craikbeca7ae2015-04-07 13:29:55 -0700212 var start = Statistics.min(events, getStart);
Chris Craik44c28202015-05-12 17:25:16 -0700213 var args = { 'Time spent': new TimeDuration(duration) };
214 args['ListView items ' + (hasInflation ? 'inflated' : 'rebound')] =
215 events.length / 2;
216 var eventInfo = hasInflation ? AndroidAuditor.ListViewInflateAlertInfo_ :
217 AndroidAuditor.ListViewBindAlertInfo_;
Chris Craik19832152015-04-16 15:43:38 -0700218 events.push(frame);
Chris Craik44c28202015-05-12 17:25:16 -0700219 return new Alert(eventInfo, start, events, args);
Chris Craikbeca7ae2015-04-07 13:29:55 -0700220 }
221
222
Chris Craik19832152015-04-16 15:43:38 -0700223 AndroidAuditor.measureLayoutAlertInfo_ = new EventInfo(
224 'Expensive measure/layout pass',
225 'Measure/Layout took a significant time, contributing to jank. Avoid triggering layout during animations.'); // @suppress longLineCheck
Chris Craikbeca7ae2015-04-07 13:29:55 -0700226 AndroidAuditor.getMeasureLayoutAlert_ = function(frame) {
Chris Craik44c28202015-05-12 17:25:16 -0700227 var events = frame.associatedEvents.filter(function(event) {
Chris Craikbeca7ae2015-04-07 13:29:55 -0700228 return event.title == 'measure' || event.title == 'layout';
229 });
Chris Craik44c28202015-05-12 17:25:16 -0700230 var duration = Statistics.sum(events, getCpuDuration);
Chris Craikbeca7ae2015-04-07 13:29:55 -0700231
232 if (events.length == 0 || duration < 3)
233 return undefined;
234
235 var start = Statistics.min(events, getStart);
Chris Craik19832152015-04-16 15:43:38 -0700236 events.push(frame);
237 return new Alert(AndroidAuditor.measureLayoutAlertInfo_, start, events,
Chris Craik44c28202015-05-12 17:25:16 -0700238 { 'Time spent': new TimeDuration(duration) });
Chris Craikbeca7ae2015-04-07 13:29:55 -0700239 }
240
241
Chris Craik19832152015-04-16 15:43:38 -0700242 AndroidAuditor.viewDrawAlertInfo_ = new EventInfo(
243 'Long View#draw()',
244 'Recording the drawing commands of invalidated Views took a long time. Avoid significant work in View or Drawable custom drawing, especially allocations or drawing to Bitmaps.'); // @suppress longLineCheck
Chris Craikbeca7ae2015-04-07 13:29:55 -0700245 AndroidAuditor.getViewDrawAlert_ = function(frame) {
246 var slice = undefined;
Chris Craik44c28202015-05-12 17:25:16 -0700247 for (var i = 0; i < frame.associatedEvents.length; i++) {
248 if (frame.associatedEvents[i].title == 'getDisplayList' ||
249 frame.associatedEvents[i].title == 'Record View#draw()') {
250 slice = frame.associatedEvents[i];
Chris Craikbeca7ae2015-04-07 13:29:55 -0700251 break;
252 }
253 }
254
Chris Craik44c28202015-05-12 17:25:16 -0700255 if (!slice || getCpuDuration(slice) < 3)
Chris Craikbeca7ae2015-04-07 13:29:55 -0700256 return undefined;
Chris Craik19832152015-04-16 15:43:38 -0700257 return new Alert(AndroidAuditor.viewDrawAlertInfo_, slice.start,
Chris Craik44c28202015-05-12 17:25:16 -0700258 [slice, frame],
259 { 'Time spent': new TimeDuration(getCpuDuration(slice)) });
Chris Craikbeca7ae2015-04-07 13:29:55 -0700260 }
261
262
Chris Craik19832152015-04-16 15:43:38 -0700263 //////////////////////////////////////////////////////////////////////////////
264 // Runtime alerts
265 //////////////////////////////////////////////////////////////////////////////
266
267 AndroidAuditor.blockingGcAlertInfo_ = new EventInfo(
268 'Blocking Garbage Collection',
269 'Blocking GCs are caused by object churn, and made worse by having large numbers of objects in the heap. Avoid allocating objects during animations/scrolling, and recycle Bitmaps to avoid triggering garbage collection.'); // @suppress longLineCheck
270 AndroidAuditor.getBlockingGcAlert_ = function(frame) {
Chris Craik44c28202015-05-12 17:25:16 -0700271 var events = frame.associatedEvents.filter(function(event) {
Chris Craik19832152015-04-16 15:43:38 -0700272 return event.title == 'DVM Suspend' ||
273 event.title == 'GC: Wait For Concurrent';
274 });
275 var blockedDuration = Statistics.sum(events, getDuration);
276 if (blockedDuration < 3)
277 return undefined;
278
279 var start = Statistics.min(events, getStart);
280 events.push(frame);
281 return new Alert(AndroidAuditor.blockingGcAlertInfo_, start, events,
Chris Craik44c28202015-05-12 17:25:16 -0700282 { 'Blocked duration': new TimeDuration(blockedDuration) });
Chris Craik19832152015-04-16 15:43:38 -0700283 };
284
285
286 AndroidAuditor.lockContentionAlertInfo_ = new EventInfo(
287 'Lock contention',
288 'UI thread lock contention is caused when another thread holds a lock that the UI thread is trying to use. UI thread progress is blocked until the lock is released. Inspect locking done within the UI thread, and ensure critical sections are short.'); // @suppress longLineCheck
289 AndroidAuditor.getLockContentionAlert_ = function(frame) {
Chris Craik44c28202015-05-12 17:25:16 -0700290 var events = frame.associatedEvents.filter(function(event) {
Chris Craik19832152015-04-16 15:43:38 -0700291 return /^Lock Contention on /.test(event.title);
292 });
293
294 var blockedDuration = Statistics.sum(events, getDuration);
295 if (blockedDuration < 1)
296 return undefined;
297
298 var start = Statistics.min(events, getStart);
299 events.push(frame);
300 return new Alert(AndroidAuditor.lockContentionAlertInfo_, start, events,
Chris Craik44c28202015-05-12 17:25:16 -0700301 { 'Blocked duration': new TimeDuration(blockedDuration) });
Chris Craik19832152015-04-16 15:43:38 -0700302 };
303
Chris Craik44c28202015-05-12 17:25:16 -0700304 AndroidAuditor.schedulingAlertInfo_ = new EventInfo(
305 'Scheduling delay',
306 'Work to produce this frame was descheduled for several milliseconds, contributing to jank. Ensure that code on the UI thread doesn\'t block on work being done on other threads, and that background threads (doing e.g. network or bitmap loading) are running at android.os.Process#THREAD_PRIORITY_BACKGROUND or lower so they are less likely to interrupt the UI thread. These background threads should with a priority number of 130 or higher in the scheduling section under the Kernel process.'); // @suppress longLineCheck
307 AndroidAuditor.getSchedulingAlert_ = function(frame) {
308 var totalDuration = 0;
309 var totalStats = {};
310 frame.threadTimeRanges.forEach(function(ttr) {
311 var stats = ttr.thread.getSchedulingStatsForRange(ttr.start, ttr.end);
312 tv.b.iterItems(stats, function(key, value) {
313 if (!(key in totalStats))
314 totalStats[key] = 0;
315 totalStats[key] += value;
316 totalDuration += value;
317 });
318 });
319
320 // only alert if frame not running for > 3ms. Note that we expect a frame
321 // to never describe intentionally idle time.
322 if (!(SCHEDULING_STATE.RUNNING in totalStats) ||
323 totalDuration == 0 ||
324 totalDuration - totalStats[SCHEDULING_STATE.RUNNING] < 3)
325 return;
326
327 var args = {};
328 tv.b.iterItems(totalStats, function(key, value) {
329 if (key === SCHEDULING_STATE.RUNNABLE)
330 key = 'Not scheduled, but runnable';
331 else if (key === SCHEDULING_STATE.UNINTR_SLEEP)
332 key = 'Blocking I/O delay';
333 args[key] = new TimeDuration(value);
334 });
335
336 return new Alert(AndroidAuditor.schedulingAlertInfo_, frame.start, [frame],
337 args);
338 };
Chris Craik19832152015-04-16 15:43:38 -0700339
Chris Craikb122baf2015-03-05 13:58:42 -0800340 AndroidAuditor.prototype = {
341 __proto__: Auditor.prototype,
342
343 renameAndSort_: function() {
Chris Craik19832152015-04-16 15:43:38 -0700344 this.model.kernel.important = false;// auto collapse
Chris Craikb122baf2015-03-05 13:58:42 -0800345 // SurfaceFlinger first, other processes sorted by slice count
346 this.model.getAllProcesses().forEach(function(process) {
347 if (this.helper.surfaceFlinger &&
348 process == this.helper.surfaceFlinger.process) {
349 if (!process.name)
350 process.name = 'SurfaceFlinger';
351 process.sortIndex = Number.NEGATIVE_INFINITY;
Chris Craik19832152015-04-16 15:43:38 -0700352 process.important = false; // auto collapse
Chris Craikb122baf2015-03-05 13:58:42 -0800353 return;
354 }
355
356 var uiThread = process.getThread(process.pid);
357 if (!process.name && uiThread && uiThread.name) {
358 if (/^ndroid\./.test(uiThread.name))
359 uiThread.name = 'a' + uiThread.name;
360 process.name = uiThread.name;
361 }
362
363 process.sortIndex = 0;
364 for (var tid in process.threads) {
365 process.sortIndex -= process.threads[tid].sliceGroup.slices.length;
366 }
367 }, this);
368
369 // ensure sequential, relative order for UI/Render/Worker threads
370 this.model.getAllThreads().forEach(function(thread) {
371 if (thread.tid == thread.parent.pid)
372 thread.sortIndex = -3;
373 if (thread.name == 'RenderThread')
374 thread.sortIndex = -2;
375 if (/^hwuiTask/.test(thread.name))
376 thread.sortIndex = -1;
377 });
378 },
379
380 pushFramesAndJudgeJank_: function() {
381 var badFramesObserved = 0;
382 var framesObserved = 0;
Chris Craik19832152015-04-16 15:43:38 -0700383 var surfaceFlinger = this.helper.surfaceFlinger;
384
Chris Craikb122baf2015-03-05 13:58:42 -0800385 this.helper.apps.forEach(function(app) {
386 // override frame list
387 app.process.frames = app.getFrames();
388
389 app.process.frames.forEach(function(frame) {
Chris Craikbeca7ae2015-04-07 13:29:55 -0700390 if (frame.totalDuration > EXPECTED_FRAME_TIME_MS * 2) {
391 badFramesObserved += 2;
392 frame.perfClass = FRAME_PERF_CLASS.TERRIBLE;
Chris Craik19832152015-04-16 15:43:38 -0700393 } else if (frame.totalDuration > EXPECTED_FRAME_TIME_MS ||
394 frameMissedDeadline(frame)) {
Chris Craikb122baf2015-03-05 13:58:42 -0800395 badFramesObserved++;
Chris Craikbeca7ae2015-04-07 13:29:55 -0700396 frame.perfClass = FRAME_PERF_CLASS.BAD;
397 } else {
398 frame.perfClass = FRAME_PERF_CLASS.GOOD;
399 }
Chris Craikb122baf2015-03-05 13:58:42 -0800400 });
401 framesObserved += app.process.frames.length;
402 });
403
404 if (framesObserved) {
405 var portionBad = badFramesObserved / framesObserved;
406 if (portionBad > 0.3)
407 this.model.faviconHue = 'red';
408 else if (portionBad > 0.05)
409 this.model.faviconHue = 'yellow';
410 else
411 this.model.faviconHue = 'green';
412 }
413 },
414
Chris Craik19832152015-04-16 15:43:38 -0700415 pushEventInfo_: function() {
416 var appAnnotator = new AppAnnotator();
417 this.helper.apps.forEach(function(app) {
418 if (app.uiThread)
419 appAnnotator.applyEventInfos(app.uiThread.sliceGroup);
420 if (app.renderThread)
421 appAnnotator.applyEventInfos(app.renderThread.sliceGroup);
422 });
423 },
424
Chris Craikb122baf2015-03-05 13:58:42 -0800425 runAnnotate: function() {
426 if (!this.helper)
427 return;
428
429 this.renameAndSort_();
430 this.pushFramesAndJudgeJank_();
Chris Craik19832152015-04-16 15:43:38 -0700431 this.pushEventInfo_();
Chris Craikb122baf2015-03-05 13:58:42 -0800432
433 this.helper.iterateImportantSlices(function(slice) {
434 slice.important = true;
435 });
436 },
437
438 runAudit: function() {
439 if (!this.helper)
440 return;
441
Chris Craikbeca7ae2015-04-07 13:29:55 -0700442 var alerts = this.model.alerts;
Chris Craikb122baf2015-03-05 13:58:42 -0800443 this.helper.apps.forEach(function(app) {
Chris Craikbeca7ae2015-04-07 13:29:55 -0700444 app.getFrames().forEach(function(frame) {
Chris Craik19832152015-04-16 15:43:38 -0700445 alerts.push.apply(alerts, AndroidAuditor.getSaveLayerAlerts_(frame));
446
447 // skip most alerts for neutral or good frames
Chris Craikbeca7ae2015-04-07 13:29:55 -0700448 if (frame.perfClass == FRAME_PERF_CLASS.NEUTRAL ||
449 frame.perfClass == FRAME_PERF_CLASS.GOOD)
450 return;
451
Chris Craik19832152015-04-16 15:43:38 -0700452 var alert = AndroidAuditor.getPathAlert_(frame);
453 if (alert)
454 alerts.push(alert);
Chris Craikbeca7ae2015-04-07 13:29:55 -0700455 var alert = AndroidAuditor.getUploadAlert_(frame);
456 if (alert)
457 alerts.push(alert);
458 var alert = AndroidAuditor.getListViewAlert_(frame);
459 if (alert)
460 alerts.push(alert);
461 var alert = AndroidAuditor.getMeasureLayoutAlert_(frame);
462 if (alert)
463 alerts.push(alert);
464 var alert = AndroidAuditor.getViewDrawAlert_(frame);
465 if (alert)
466 alerts.push(alert);
Chris Craik19832152015-04-16 15:43:38 -0700467 var alert = AndroidAuditor.getBlockingGcAlert_(frame);
468 if (alert)
469 alerts.push(alert);
470 var alert = AndroidAuditor.getLockContentionAlert_(frame);
471 if (alert)
472 alerts.push(alert);
Chris Craik44c28202015-05-12 17:25:16 -0700473 var alert = AndroidAuditor.getSchedulingAlert_(frame);
474 if (alert)
475 alerts.push(alert);
Chris Craikbeca7ae2015-04-07 13:29:55 -0700476 });
Chris Craikb122baf2015-03-05 13:58:42 -0800477 }, this);
478
479 this.addRenderingInteractionRecords();
480 this.addInputInteractionRecords();
481 },
482
Chris Craikb122baf2015-03-05 13:58:42 -0800483 addRenderingInteractionRecords: function() {
484 var events = [];
485 this.helper.apps.forEach(function(app) {
486 events.push.apply(events, app.getAnimationAsyncSlices());
487 events.push.apply(events, app.getFrames());
488 });
489
490 var mergerFunction = function(events) {
Chris Craikbeca7ae2015-04-07 13:29:55 -0700491 var ir = new InteractionRecord('Rendering',
Chris Craikb122baf2015-03-05 13:58:42 -0800492 tv.b.ui.getColorIdForGeneralPurposeString('mt_rendering'),
493 events[0].start,
494 events[events.length - 1].end - events[0].start);
495 this.model.addInteractionRecord(ir);
496 }.bind(this);
497 tv.e.audits.mergeEvents(events, 30, mergerFunction);
498 },
499
500 addInputInteractionRecords: function() {
501 var inputSamples = [];
502 this.helper.apps.forEach(function(app) {
503 inputSamples.push.apply(inputSamples, app.getInputSamples());
504 });
505
506 var mergerFunction = function(events) {
Chris Craikbeca7ae2015-04-07 13:29:55 -0700507 var ir = new InteractionRecord('Input',
Chris Craikb122baf2015-03-05 13:58:42 -0800508 tv.b.ui.getColorIdForGeneralPurposeString('mt_input'),
509 events[0].timestamp,
510 events[events.length - 1].timestamp - events[0].timestamp);
511 this.model.addInteractionRecord(ir);
512 }.bind(this);
513 var timestampFunction = function(x) { return x.timestamp; };
514 tv.e.audits.mergeEvents(inputSamples, 30, mergerFunction,
515 timestampFunction, timestampFunction);
516 }
517 };
518
519 Auditor.register(AndroidAuditor);
520
Chris Craik19832152015-04-16 15:43:38 -0700521 function AppAnnotator() {
522 this.titleInfoLookup = {};
523 this.titleParentLookup = {};
524 this.build_();
525 }
526
527 AppAnnotator.prototype = {
528 build_: function() {
529 var registerEventInfo = function(dict) {
530 this.titleInfoLookup[dict.title] = new EventInfo(
531 dict.title, dict.description, dict.docUrl);
532 if (dict.parent)
533 this.titleParentLookup[dict.title] = dict.parent;
534 }.bind(this);
535
Chris Craik44c28202015-05-12 17:25:16 -0700536 registerEventInfo({
537 title: 'inflate',
538 description: 'Constructing a View hierarchy from pre-processed XML via LayoutInflater#layout. This includes constructing all of the View objects in the hierarchy, and applying styled attributes.'}); // @suppress longLineCheck
539
Chris Craik19832152015-04-16 15:43:38 -0700540 //////////////////////////////////////////////////////////////////////////
541 // Adapter view
542 //////////////////////////////////////////////////////////////////////////
543 registerEventInfo({
544 title: 'obtainView',
545 description: 'Adapter#getView() called to bind content to a recycled View that is being presented.'}); // @suppress longLineCheck
546 registerEventInfo({
547 title: 'setupListItem',
548 description: 'Attached a newly-bound, recycled View to its parent ListView.'}); // @suppress longLineCheck
549 registerEventInfo({
550 title: 'setupGridItem',
551 description: 'Attached a newly-bound, recycled View to its parent GridView.'}); // @suppress longLineCheck
552
553 //////////////////////////////////////////////////////////////////////////
554 // performTraversals + sub methods
555 //////////////////////////////////////////////////////////////////////////
556 registerEventInfo({
557 title: 'performTraversals',
558 description: 'A drawing traversal of the View hierarchy, comprised of all layout and drawing needed to produce the frame.'}); // @suppress longLineCheck
559 registerEventInfo({
560 title: 'measure',
561 parent: 'performTraversals',
562 docUrl: 'https://developer.android.com/reference/android/view/View.html#Layout', // @suppress longLineCheck
563 description: 'First of two phases in view hierarchy layout. Views are asked to size themselves according to constraints supplied by their parent. Some ViewGroups may measure a child more than once to help satisfy their own constraints. Nesting ViewGroups that measure children more than once can lead to excessive and repeated work.'}); // @suppress longLineCheck
564 registerEventInfo({
565 title: 'layout',
566 parent: 'performTraversals',
567 docUrl: 'https://developer.android.com/reference/android/view/View.html#Layout', // @suppress longLineCheck
568 description: 'Second of two phases in view hierarchy layout, repositioning content and child Views into their new locations.'}); // @suppress longLineCheck
569 registerEventInfo({
570 title: 'draw',
571 parent: 'performTraversals',
572 description: 'Draw pass over the View hierarchy. Every invalidated View will have its drawing commands recorded. On Android versions prior to Lollipop, this would also include the issuing of draw commands to the GPU. Starting with Lollipop, it only includes the recording of commands, and syncing that information to the RenderThread.'}); // @suppress longLineCheck
573
574 var recordString = 'Every invalidated View\'s drawing commands are recorded. Each will have View#draw() called, and is passed a Canvas that will record and store its drawing commands until it is next invalidated/rerecorded.'; // @suppress longLineCheck
575 registerEventInfo({
576 title: 'getDisplayList', // Legacy name for compatibility.
577 parent: 'draw',
578 description: recordString});
579 registerEventInfo({
580 title: 'Record View#draw()',
581 parent: 'draw',
582 description: recordString});
583
584
585 registerEventInfo({
586 title: 'drawDisplayList',
587 parent: 'draw',
588 description: 'Execution of recorded draw commands to generate a frame. This represents the actual formation and issuing of drawing commands to the GPU.'}); // @suppress longLineCheck
589
590 //////////////////////////////////////////////////////////////////////////
591 // RenderThread
592 //////////////////////////////////////////////////////////////////////////
593 registerEventInfo({
594 title: 'DrawFrame',
595 description: 'RenderThread portion of the standard UI/RenderThread split frame. This represents the actual formation and issuing of drawing commands to the GPU.'}); // @suppress longLineCheck
596 registerEventInfo({
597 title: 'doFrame',
598 description: 'RenderThread animation frame. Represents drawing work done by the RenderThread on a frame where the UI thread did not produce new drawing content.'}); // @suppress longLineCheck
599 registerEventInfo({
600 title: 'syncFrameState',
601 description: 'Sync stage between the UI thread and the RenderThread, where the UI thread hands off a frame (including information about modified Views). Time in this method primarily consists of uploading modified Bitmaps to the GPU. After this sync is completed, the UI thread is unblocked, and the RenderThread starts to render the frame.'}); // @suppress longLineCheck
602 registerEventInfo({
603 title: 'flush drawing commands',
604 description: 'Issuing the now complete drawing commands to the GPU.'}); // @suppress longLineCheck
605 registerEventInfo({
606 title: 'eglSwapBuffers',
607 description: 'Complete GPU rendering of the frame.'}); // @suppress longLineCheck
Chris Craik44c28202015-05-12 17:25:16 -0700608
609 //////////////////////////////////////////////////////////////////////////
610 // RecyclerView
611 //////////////////////////////////////////////////////////////////////////
612 registerEventInfo({
613 title: 'RV Scroll',
614 description: 'RecyclerView is calculating a scroll. If there are too many of these in Systrace, some Views inside RecyclerView might be causing it. Try to avoid using EditText, focusable views or handle them with care.'}); // @suppress longLineCheck
615 registerEventInfo({
616 title: 'RV OnLayout',
617 description: 'OnLayout has been called by the View system. If this shows up too many times in Systrace, make sure the children of RecyclerView do not update themselves directly. This will cause a full re-layout but when it happens via the Adapter notifyItemChanged, RecyclerView can avoid full layout calculation.'}); // @suppress longLineCheck
618 registerEventInfo({
619 title: 'RV FullInvalidate',
620 description: 'NotifyDataSetChanged or equal has been called. If this is taking a long time, try sending granular notify adapter changes instead of just calling notifyDataSetChanged or setAdapter / swapAdapter. Adding stable ids to your adapter might help.'}); // @suppress longLineCheck
621 registerEventInfo({
622 title: 'RV PartialInvalidate',
623 description: 'RecyclerView is rebinding a View. If this is taking a lot of time, consider optimizing your layout or make sure you are not doing extra operations in onBindViewHolder call.'}); // @suppress longLineCheck
624 registerEventInfo({
625 title: 'RV OnBindView',
626 description: 'RecyclerView is rebinding a View. If this is taking a lot of time, consider optimizing your layout or make sure you are not doing extra operations in onBindViewHolder call.'}); // @suppress longLineCheck
627 registerEventInfo({
628 title: 'RV CreateView',
629 description: 'RecyclerView is creating a new View. If too many of these are present: 1) There might be a problem in Recycling (e.g. custom Animations that set transient state and prevent recycling or ItemAnimator not implementing the contract properly. See Adapter#onFailedToRecycleView(ViewHolder). 2) There may be too many item view types. Try merging them. 3) There might be too many itemChange animations and not enough space in RecyclerPool. Try increasing your pool size and item cache size.'}); // @suppress longLineCheck
630
631 //////////////////////////////////////////////////////////////////////////
632 // Graphics + Composition
633 //////////////////////////////////////////////////////////////////////////
634 // TODO(ccraik): SurfaceFlinger work
635 registerEventInfo({
636 title: 'eglSwapBuffers',
637 description: 'The CPU has finished producing drawing commands, and is flushing drawing work to the GPU, and posting that buffer to the consumer (which is often SurfaceFlinger window composition). Once this is completed, the GPU can produce the frame content without any involvement from the CPU.'}); // @suppress longLineCheck
Chris Craik19832152015-04-16 15:43:38 -0700638 },
639
640 applyEventInfosRecursive_: function(parentNames, slice) {
641 // Set EventInfo on the slice if it matches title, and parent.
642 if (slice.title in this.titleInfoLookup) {
643 var expectedParentName = this.titleParentLookup[slice.title];
644 if (!expectedParentName || expectedParentName in parentNames)
645 slice.info = this.titleInfoLookup[slice.title];
646 }
647
648 // Push slice into parentNames, and recurse over subSlices.
649 if (slice.subSlices.length > 0) {
650 // Increment title in parentName dict.
651 if (!(slice.title in parentNames))
652 parentNames[slice.title] = 0;
653 parentNames[slice.title]++;
654
655 // Recurse over subSlices.
656 slice.subSlices.forEach(function(subSlice) {
657 this.applyEventInfosRecursive_(parentNames, subSlice);
658 }, this);
659
660 // Decrement title in parentName dict.
661 parentNames[slice.title]--;
662 if (parentNames[slice.title] == 0)
663 delete parentNames[slice.title];
664 }
665 },
666
667 applyEventInfos: function(sliceGroup) {
668 sliceGroup.topLevelSlices.forEach(function(slice) {
669 this.applyEventInfosRecursive_({}, slice);
670 }, this);
671 }
672 };
673
Chris Craikb122baf2015-03-05 13:58:42 -0800674 return {
675 AndroidAuditor: AndroidAuditor
676 };
677});
678</script>