• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!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-->
7
8<link rel="import" href="/tracing/base/iteration_helpers.html">
9<link rel="import" href="/tracing/base/range_utils.html">
10<link rel="import" href="/tracing/base/statistics.html">
11<link rel="import" href="/tracing/core/auditor.html">
12<link rel="import" href="/tracing/model/alert.html">
13<link rel="import" href="/tracing/model/frame.html">
14<link rel="import" href="/tracing/model/helpers/android_model_helper.html">
15<link rel="import" href="/tracing/model/thread_time_slice.html">
16<link rel="import" href="/tracing/model/user_model/response_expectation.html">
17<link rel="import" href="/tracing/value/numeric.html">
18<link rel="import" href="/tracing/value/unit.html">
19
20<script>
21'use strict';
22
23/**
24 * @fileoverview Class for Android-specific Auditing.
25 */
26tr.exportTo('tr.e.audits', function() {
27  var SCHEDULING_STATE = tr.model.SCHEDULING_STATE;
28  var Auditor = tr.c.Auditor;
29  var AndroidModelHelper = tr.model.helpers.AndroidModelHelper;
30  var ColorScheme = tr.b.ColorScheme;
31  var Statistics = tr.b.Statistics;
32  var FRAME_PERF_CLASS = tr.model.FRAME_PERF_CLASS;
33  var Alert = tr.model.Alert;
34  var EventInfo = tr.model.EventInfo;
35  var ScalarNumeric = tr.v.ScalarNumeric;
36  var timeDurationInMs = tr.v.Unit.byName.timeDurationInMs;
37
38  // TODO: extract from VSYNC, since not all devices have vsync near 60fps
39  var EXPECTED_FRAME_TIME_MS = 16.67;
40
41  function getStart(e) { return e.start; }
42  function getDuration(e) { return e.duration; }
43  // used for general UI thread responsiveness alerts, falls back to duration
44  function getCpuDuration(e) {
45    return (e.cpuDuration !== undefined) ? e.cpuDuration : e.duration;
46  }
47
48  function frameIsActivityStart(frame) {
49    for (var i = 0; i < frame.associatedEvents.length; i++) {
50      if (frame.associatedEvents[i].title == 'activityStart')
51        return true;
52    }
53    return false;
54  }
55
56  function frameMissedDeadline(frame) {
57    return frame.args['deadline'] && frame.args['deadline'] < frame.end;
58  }
59
60  /** Builder object for EventInfo docLink structures */
61  function DocLinkBuilder() {
62    this.docLinks = [];
63  }
64  DocLinkBuilder.prototype = {
65    addAppVideo: function(name, videoId) {
66      this.docLinks.push({
67        label: 'Video Link',
68        textContent: ('Android Performance Patterns: ' + name),
69        href: 'https://www.youtube.com/watch?list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE&v=' + videoId // @suppress longLineCheck
70      });
71      return this;
72    },
73    addDacRef: function(name, link) {
74      this.docLinks.push({
75          label: 'Doc Link',
76          textContent: (name + ' documentation'),
77          href: 'https://developer.android.com/reference/' + link
78      });
79      return this;
80    },
81    build: function() {
82      return this.docLinks;
83    }
84  };
85
86  /**
87   * Auditor for Android-specific traces.
88   * @constructor
89   */
90  function AndroidAuditor(model) {
91    Auditor.call(this, model);
92
93    var helper = model.getOrCreateHelper(AndroidModelHelper);
94    if (helper.apps.length || helper.surfaceFlinger)
95      this.helper = helper;
96  };
97
98  //////////////////////////////////////////////////////////////////////////////
99  // Rendering / RenderThread alerts - only available on SDK 22+
100  //////////////////////////////////////////////////////////////////////////////
101
102  AndroidAuditor.viewAlphaAlertInfo_ = new EventInfo(
103      'Inefficient View alpha usage',
104      'Setting an alpha between 0 and 1 has significant performance costs, if one of the fast alpha paths is not used.', // @suppress longLineCheck
105      new DocLinkBuilder()
106          .addAppVideo('Hidden Cost of Transparency', 'wIy8g8yNhNk')
107          .addDacRef('View#setAlpha()', 'android/view/View.html#setAlpha(float)') // @suppress longLineCheck
108          .build());
109  AndroidAuditor.saveLayerAlertInfo_ = new EventInfo(
110      'Expensive rendering with Canvas#saveLayer()',
111      '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
112      new DocLinkBuilder()
113          .addAppVideo('Hidden Cost of Transparency', 'wIy8g8yNhNk')
114          .addDacRef('Canvas#saveLayerAlpha()', 'android/graphics/Canvas.html#saveLayerAlpha(android.graphics.RectF, int, int)') // @suppress longLineCheck
115          .build());
116  AndroidAuditor.getSaveLayerAlerts_ = function(frame) {
117    var badAlphaRegEx =
118        /^(.+) alpha caused (unclipped )?saveLayer (\d+)x(\d+)$/;
119    var saveLayerRegEx = /^(unclipped )?saveLayer (\d+)x(\d+)$/;
120
121    var ret = [];
122    var events = [];
123
124    frame.associatedEvents.forEach(function(slice) {
125      var match = badAlphaRegEx.exec(slice.title);
126      if (match) {
127        // due to bug in tracing code on SDK 22, ignore
128        // presence of 'unclipped' string in View alpha slices
129        var args = { 'view name': match[1],
130                     width: parseInt(match[3]),
131                     height: parseInt(match[4]) };
132        ret.push(new Alert(AndroidAuditor.viewAlphaAlertInfo_,
133                           slice.start, [slice], args));
134      } else if (saveLayerRegEx.test(slice.title))
135        events.push(slice);
136    }, this);
137
138    if (events.length > ret.length) {
139      // more saveLayers than bad alpha can account for - add another alert
140
141      var unclippedSeen = Statistics.sum(events, function(slice) {
142        return saveLayerRegEx.exec(slice.title)[1] ? 1 : 0;
143      });
144      var clippedSeen = events.length - unclippedSeen;
145      var earliestStart = Statistics.min(events, function(slice) {
146        return slice.start;
147      });
148
149      var args = {
150        'Unclipped saveLayer count (especially bad!)': unclippedSeen,
151        'Clipped saveLayer count': clippedSeen
152      };
153
154      events.push(frame);
155      ret.push(new Alert(AndroidAuditor.saveLayerAlertInfo_,
156                         earliestStart, events, args));
157    }
158
159    return ret;
160  };
161
162
163  AndroidAuditor.pathAlertInfo_ = new EventInfo(
164      'Path texture churn',
165      '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
166  AndroidAuditor.getPathAlert_ = function(frame) {
167    var uploadRegEx = /^Generate Path Texture$/;
168
169    var events = frame.associatedEvents.filter(function(event) {
170      return event.title == 'Generate Path Texture';
171    });
172    var start = Statistics.min(events, getStart);
173    var duration = Statistics.sum(events, getDuration);
174
175    if (duration < 3)
176      return undefined;
177
178    events.push(frame);
179    return new Alert(AndroidAuditor.pathAlertInfo_, start, events,
180        { 'Time spent': new ScalarNumeric(timeDurationInMs, duration) });
181  };
182
183
184  AndroidAuditor.uploadAlertInfo_ = new EventInfo(
185      'Expensive Bitmap uploads',
186      '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
187  AndroidAuditor.getUploadAlert_ = function(frame) {
188    var uploadRegEx = /^Upload (\d+)x(\d+) Texture$/;
189
190    var events = [];
191    var start = Number.POSITIVE_INFINITY;
192    var duration = 0;
193    var pixelsUploaded = 0;
194    frame.associatedEvents.forEach(function(event) {
195      var match = uploadRegEx.exec(event.title);
196      if (match) {
197        events.push(event);
198        start = Math.min(start, event.start);
199        duration += event.duration;
200        pixelsUploaded += parseInt(match[1]) * parseInt(match[2]);
201      }
202    });
203    if (events.length == 0 || duration < 3)
204      return undefined;
205
206    var mPixels = (pixelsUploaded / 1000000).toFixed(2) + ' million';
207    var args = { 'Pixels uploaded': mPixels,
208                 'Time spent': new ScalarNumeric(timeDurationInMs, duration) };
209    events.push(frame);
210    return new Alert(AndroidAuditor.uploadAlertInfo_, start, events, args);
211  };
212
213  //////////////////////////////////////////////////////////////////////////////
214  // UI responsiveness alerts
215  //////////////////////////////////////////////////////////////////////////////
216
217  AndroidAuditor.ListViewInflateAlertInfo_ = new EventInfo(
218      'Inflation during ListView recycling',
219      'ListView item recycling involved inflating views. Ensure your Adapter#getView() recycles the incoming View, instead of constructing a new one.'); // @suppress longLineCheck
220  AndroidAuditor.ListViewBindAlertInfo_ = new EventInfo(
221      'Inefficient ListView recycling/rebinding',
222      'ListView recycling taking too much time per frame. Ensure your Adapter#getView() binds data efficiently.'); // @suppress longLineCheck
223  AndroidAuditor.getListViewAlert_ = function(frame) {
224    var events = frame.associatedEvents.filter(function(event) {
225      return event.title == 'obtainView' || event.title == 'setupListItem';
226    });
227    var duration = Statistics.sum(events, getCpuDuration);
228
229    if (events.length == 0 || duration < 3)
230      return undefined;
231
232    // simplifying assumption - check for *any* inflation.
233    // TODO(ccraik): make 'inflate' slices associated events.
234    var hasInflation = false;
235    for (var i = 0; i < events.length; i++) {
236      if (events[i] instanceof tr.model.Slice &&
237          events[i].findDescendentSlice('inflate')) {
238        hasInflation = true;
239        break;
240      }
241    }
242
243    var start = Statistics.min(events, getStart);
244    var args = { 'Time spent': new ScalarNumeric(timeDurationInMs, duration) };
245    args['ListView items ' + (hasInflation ? 'inflated' : 'rebound')] =
246        events.length / 2;
247    var eventInfo = hasInflation ? AndroidAuditor.ListViewInflateAlertInfo_ :
248        AndroidAuditor.ListViewBindAlertInfo_;
249    events.push(frame);
250    return new Alert(eventInfo, start, events, args);
251  };
252
253
254  AndroidAuditor.measureLayoutAlertInfo_ = new EventInfo(
255      'Expensive measure/layout pass',
256      'Measure/Layout took a significant time, contributing to jank. Avoid triggering layout during animations.', // @suppress longLineCheck
257      new DocLinkBuilder()
258          .addAppVideo('Invalidations, Layouts, and Performance', 'we6poP0kw6E')
259          .build());
260  AndroidAuditor.getMeasureLayoutAlert_ = function(frame) {
261    var events = frame.associatedEvents.filter(function(event) {
262      return event.title == 'measure' || event.title == 'layout';
263    });
264    var duration = Statistics.sum(events, getCpuDuration);
265
266    if (events.length == 0 || duration < 3)
267      return undefined;
268
269    var start = Statistics.min(events, getStart);
270    events.push(frame);
271    return new Alert(AndroidAuditor.measureLayoutAlertInfo_, start, events,
272        { 'Time spent': new ScalarNumeric(timeDurationInMs, duration) });
273  };
274
275
276  AndroidAuditor.viewDrawAlertInfo_ = new EventInfo(
277      'Long View#draw()',
278      '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
279      new DocLinkBuilder()
280          .addAppVideo('Invalidations, Layouts, and Performance', 'we6poP0kw6E')
281          .addAppVideo('Avoiding Allocations in onDraw()', 'HAK5acHQ53E')
282          .build());
283  AndroidAuditor.getViewDrawAlert_ = function(frame) {
284    var slice = undefined;
285    for (var i = 0; i < frame.associatedEvents.length; i++) {
286      if (frame.associatedEvents[i].title == 'getDisplayList' ||
287          frame.associatedEvents[i].title == 'Record View#draw()') {
288        slice = frame.associatedEvents[i];
289        break;
290      }
291    }
292
293    if (!slice || getCpuDuration(slice) < 3)
294      return undefined;
295    return new Alert(AndroidAuditor.viewDrawAlertInfo_, slice.start,
296        [slice, frame],
297        { 'Time spent': new ScalarNumeric(
298            timeDurationInMs, getCpuDuration(slice)) });
299  };
300
301
302  //////////////////////////////////////////////////////////////////////////////
303  // Runtime alerts
304  //////////////////////////////////////////////////////////////////////////////
305
306  AndroidAuditor.blockingGcAlertInfo_ = new EventInfo(
307      'Blocking Garbage Collection',
308      '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
309      new DocLinkBuilder()
310          .addAppVideo('Garbage Collection in Android', 'pzfzz50W5Uo')
311          .addAppVideo('Avoiding Allocations in onDraw()', 'HAK5acHQ53E')
312          .build());
313  AndroidAuditor.getBlockingGcAlert_ = function(frame) {
314    var events = frame.associatedEvents.filter(function(event) {
315      return event.title == 'DVM Suspend' ||
316          event.title == 'GC: Wait For Concurrent';
317    });
318    var blockedDuration = Statistics.sum(events, getDuration);
319    if (blockedDuration < 3)
320      return undefined;
321
322    var start = Statistics.min(events, getStart);
323    events.push(frame);
324    return new Alert(AndroidAuditor.blockingGcAlertInfo_, start, events,
325        { 'Blocked duration': new ScalarNumeric(
326            timeDurationInMs, blockedDuration) });
327  };
328
329
330  AndroidAuditor.lockContentionAlertInfo_ = new EventInfo(
331      'Lock contention',
332      '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
333  AndroidAuditor.getLockContentionAlert_ = function(frame) {
334    var events = frame.associatedEvents.filter(function(event) {
335      return /^Lock Contention on /.test(event.title);
336    });
337
338    var blockedDuration = Statistics.sum(events, getDuration);
339    if (blockedDuration < 1)
340      return undefined;
341
342    var start = Statistics.min(events, getStart);
343    events.push(frame);
344    return new Alert(AndroidAuditor.lockContentionAlertInfo_, start, events,
345        { 'Blocked duration': new ScalarNumeric(
346            timeDurationInMs, blockedDuration) });
347  };
348
349  AndroidAuditor.schedulingAlertInfo_ = new EventInfo(
350      'Scheduling delay',
351      '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 show up with a priority number of 130 or higher in the scheduling section under the Kernel process.'); // @suppress longLineCheck
352  AndroidAuditor.getSchedulingAlert_ = function(frame) {
353    var totalDuration = 0;
354    var totalStats = {};
355    frame.threadTimeRanges.forEach(function(ttr) {
356      var stats = ttr.thread.getSchedulingStatsForRange(ttr.start, ttr.end);
357      tr.b.iterItems(stats, function(key, value) {
358        if (!(key in totalStats))
359          totalStats[key] = 0;
360        totalStats[key] += value;
361        totalDuration += value;
362      });
363    });
364
365    // only alert if frame not running for > 3ms. Note that we expect a frame
366    // to never describe intentionally idle time.
367    if (!(SCHEDULING_STATE.RUNNING in totalStats) ||
368        totalDuration == 0 ||
369        totalDuration - totalStats[SCHEDULING_STATE.RUNNING] < 3)
370      return;
371
372    var args = {};
373    tr.b.iterItems(totalStats, function(key, value) {
374      if (key === SCHEDULING_STATE.RUNNABLE)
375        key = 'Not scheduled, but runnable';
376      else if (key === SCHEDULING_STATE.UNINTR_SLEEP)
377        key = 'Blocking I/O delay';
378      args[key] = new ScalarNumeric(timeDurationInMs, value);
379    });
380
381    return new Alert(AndroidAuditor.schedulingAlertInfo_, frame.start, [frame],
382                     args);
383  };
384
385  AndroidAuditor.prototype = {
386    __proto__: Auditor.prototype,
387
388    renameAndSort_: function() {
389      this.model.kernel.important = false;// auto collapse
390      // SurfaceFlinger first, other processes sorted by slice count
391      this.model.getAllProcesses().forEach(function(process) {
392        if (this.helper.surfaceFlinger &&
393            process == this.helper.surfaceFlinger.process) {
394          if (!process.name)
395            process.name = 'SurfaceFlinger';
396          process.sortIndex = Number.NEGATIVE_INFINITY;
397          process.important = false; // auto collapse
398          return;
399        }
400
401        var uiThread = process.getThread(process.pid);
402        if (!process.name && uiThread && uiThread.name) {
403          if (/^ndroid\./.test(uiThread.name))
404            uiThread.name = 'a' + uiThread.name;
405          process.name = uiThread.name;
406
407          uiThread.name = 'UI Thread';
408        }
409
410        process.sortIndex = 0;
411        for (var tid in process.threads) {
412          process.sortIndex -= process.threads[tid].sliceGroup.slices.length;
413        }
414      }, this);
415
416      // ensure sequential, relative order for UI/Render/Worker threads
417      this.model.getAllThreads().forEach(function(thread) {
418        if (thread.tid == thread.parent.pid)
419          thread.sortIndex = -3;
420        if (thread.name == 'RenderThread')
421          thread.sortIndex = -2;
422        if (/^hwuiTask/.test(thread.name))
423          thread.sortIndex = -1;
424      });
425    },
426
427    pushFramesAndJudgeJank_: function() {
428      var badFramesObserved = 0;
429      var framesObserved = 0;
430      var surfaceFlinger = this.helper.surfaceFlinger;
431
432      this.helper.apps.forEach(function(app) {
433        // override frame list
434        app.process.frames = app.getFrames();
435
436        app.process.frames.forEach(function(frame) {
437          if (frame.totalDuration > EXPECTED_FRAME_TIME_MS * 2) {
438            badFramesObserved += 2;
439            frame.perfClass = FRAME_PERF_CLASS.TERRIBLE;
440          } else if (frame.totalDuration > EXPECTED_FRAME_TIME_MS ||
441              frameMissedDeadline(frame)) {
442            badFramesObserved++;
443            frame.perfClass = FRAME_PERF_CLASS.BAD;
444          } else {
445            frame.perfClass = FRAME_PERF_CLASS.GOOD;
446          }
447        });
448        framesObserved += app.process.frames.length;
449      });
450
451      if (framesObserved) {
452        var portionBad = badFramesObserved / framesObserved;
453        if (portionBad > 0.3)
454          this.model.faviconHue = 'red';
455        else if (portionBad > 0.05)
456          this.model.faviconHue = 'yellow';
457        else
458          this.model.faviconHue = 'green';
459      }
460    },
461
462    pushEventInfo_: function() {
463      var appAnnotator = new AppAnnotator();
464      this.helper.apps.forEach(function(app) {
465        if (app.uiThread)
466          appAnnotator.applyEventInfos(app.uiThread.sliceGroup);
467        if (app.renderThread)
468          appAnnotator.applyEventInfos(app.renderThread.sliceGroup);
469      });
470    },
471
472    runAnnotate: function() {
473      if (!this.helper)
474        return;
475
476      this.renameAndSort_();
477      this.pushFramesAndJudgeJank_();
478      this.pushEventInfo_();
479
480      this.helper.iterateImportantSlices(function(slice) {
481        slice.important = true;
482      });
483    },
484
485    runAudit: function() {
486      if (!this.helper)
487        return;
488
489      var alerts = this.model.alerts;
490      this.helper.apps.forEach(function(app) {
491        app.getFrames().forEach(function(frame) {
492          alerts.push.apply(alerts, AndroidAuditor.getSaveLayerAlerts_(frame));
493
494          // skip most alerts for neutral or good frames
495          if (frame.perfClass == FRAME_PERF_CLASS.NEUTRAL ||
496              frame.perfClass == FRAME_PERF_CLASS.GOOD)
497            return;
498
499          var alert = AndroidAuditor.getPathAlert_(frame);
500          if (alert)
501            alerts.push(alert);
502          var alert = AndroidAuditor.getUploadAlert_(frame);
503          if (alert)
504            alerts.push(alert);
505          var alert = AndroidAuditor.getListViewAlert_(frame);
506          if (alert)
507            alerts.push(alert);
508          var alert = AndroidAuditor.getMeasureLayoutAlert_(frame);
509          if (alert)
510            alerts.push(alert);
511          var alert = AndroidAuditor.getViewDrawAlert_(frame);
512          if (alert)
513            alerts.push(alert);
514          var alert = AndroidAuditor.getBlockingGcAlert_(frame);
515          if (alert)
516            alerts.push(alert);
517          var alert = AndroidAuditor.getLockContentionAlert_(frame);
518          if (alert)
519            alerts.push(alert);
520          var alert = AndroidAuditor.getSchedulingAlert_(frame);
521          if (alert)
522            alerts.push(alert);
523        });
524      }, this);
525
526      this.addRenderingInteractionRecords();
527      this.addInputInteractionRecords();
528    },
529
530    addRenderingInteractionRecords: function() {
531      var events = [];
532      this.helper.apps.forEach(function(app) {
533        events.push.apply(events, app.getAnimationAsyncSlices());
534        events.push.apply(events, app.getFrames());
535      });
536
537      var mergerFunction = function(events) {
538        var ir = new tr.model.um.ResponseExpectation(
539            this.model, 'Rendering',
540            events[0].min,
541            events[events.length - 1].max - events[0].min);
542        this.model.userModel.expectations.push(ir);
543      }.bind(this);
544      tr.b.mergeRanges(tr.b.convertEventsToRanges(events), 30, mergerFunction);
545    },
546
547    addInputInteractionRecords: function() {
548      var inputSamples = [];
549      this.helper.apps.forEach(function(app) {
550        inputSamples.push.apply(inputSamples, app.getInputSamples());
551      });
552
553      var mergerFunction = function(events) {
554        var ir = new tr.model.um.ResponseExpectation(
555            this.model, 'Input',
556            events[0].min,
557            events[events.length - 1].max - events[0].min);
558        this.model.userModel.expectations.push(ir);
559      }.bind(this);
560      var inputRanges = inputSamples.map(function(sample) {
561        return tr.b.Range.fromExplicitRange(sample.timestamp, sample.timestamp);
562      });
563      tr.b.mergeRanges(inputRanges, 30, mergerFunction);
564    }
565  };
566
567  Auditor.register(AndroidAuditor);
568
569  function AppAnnotator() {
570    this.titleInfoLookup = {};
571    this.titleParentLookup = {};
572    this.build_();
573  }
574
575  AppAnnotator.prototype = {
576    build_: function() {
577      var registerEventInfo = function(dict) {
578        this.titleInfoLookup[dict.title] = new EventInfo(
579            dict.title, dict.description, dict.docLinks);
580        if (dict.parents)
581          this.titleParentLookup[dict.title] = dict.parents;
582      }.bind(this);
583
584      registerEventInfo({
585          title: 'inflate',
586          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
587
588      //////////////////////////////////////////////////////////////////////////
589      // Adapter view
590      //////////////////////////////////////////////////////////////////////////
591      registerEventInfo({
592          title: 'obtainView',
593          description: 'Adapter#getView() called to bind content to a recycled View that is being presented.'}); // @suppress longLineCheck
594      registerEventInfo({
595          title: 'setupListItem',
596          description: 'Attached a newly-bound, recycled View to its parent ListView.'}); // @suppress longLineCheck
597      registerEventInfo({
598          title: 'setupGridItem',
599          description: 'Attached a newly-bound, recycled View to its parent GridView.'}); // @suppress longLineCheck
600
601      //////////////////////////////////////////////////////////////////////////
602      // Choreographer (tracing enabled on M+)
603      //////////////////////////////////////////////////////////////////////////
604      var choreographerLinks = new DocLinkBuilder()
605          .addDacRef('Choreographer', 'android/view/Choreographer.html') // @suppress longLineCheck
606          .build();
607      registerEventInfo({
608          title: 'Choreographer#doFrame',
609          docLinks: choreographerLinks,
610          description: 'Choreographer executes frame callbacks for inputs, animations, and rendering traversals. When this work is done, a frame will be presented to the user.'}); // @suppress longLineCheck
611      registerEventInfo({
612          title: 'input',
613          parents: ['Choreographer#doFrame'],
614          docLinks: choreographerLinks,
615          description: 'Input callbacks are processed. This generally encompasses dispatching input to Views, as well as any work the Views do to process this input/gesture.'}); // @suppress longLineCheck
616      registerEventInfo({
617          title: 'animation',
618          parents: ['Choreographer#doFrame'],
619          docLinks: choreographerLinks,
620          description: 'Animation callbacks are processed. This is generally minimal work, as animations determine progress for the frame, and push new state to animated objects (such as setting View properties).'}); // @suppress longLineCheck
621      registerEventInfo({
622          title: 'traversals',
623          parents: ['Choreographer#doFrame'],
624          docLinks: choreographerLinks,
625          description: 'Primary draw traversals. This is the primary traversal of the View hierarchy, including layout and draw passes.'}); // @suppress longLineCheck
626
627      //////////////////////////////////////////////////////////////////////////
628      // performTraversals + sub methods
629      //////////////////////////////////////////////////////////////////////////
630      var traversalParents = ['Choreographer#doFrame', 'performTraversals'];
631      var layoutLinks = new DocLinkBuilder()
632          .addDacRef('View#Layout', 'android/view/View.html#Layout')
633          .build();
634      registerEventInfo({
635          title: 'performTraversals',
636          description: 'A drawing traversal of the View hierarchy, comprised of all layout and drawing needed to produce the frame.'}); // @suppress longLineCheck
637      registerEventInfo({
638          title: 'measure',
639          parents: traversalParents,
640          docLinks: layoutLinks,
641          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
642      registerEventInfo({
643          title: 'layout',
644          parents: traversalParents,
645          docLinks: layoutLinks,
646          description: 'Second of two phases in view hierarchy layout, repositioning content and child Views into their new locations.'}); // @suppress longLineCheck
647      var drawString = '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
648      registerEventInfo({
649          title: 'draw',
650          parents: traversalParents,
651          description: drawString});
652
653      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
654      registerEventInfo({
655          title: 'getDisplayList', // Legacy name for compatibility.
656          parents: ['draw'],
657          description: recordString});
658      registerEventInfo({
659          title: 'Record View#draw()',
660          parents: ['draw'],
661          description: recordString});
662
663      registerEventInfo({
664          title: 'drawDisplayList',
665          parents: ['draw'],
666          description: 'Execution of recorded draw commands to generate a frame. This represents the actual formation and issuing of drawing commands to the GPU. On Android L and higher devices, this work is done on a dedicated RenderThread, instead of on the UI Thread.'}); // @suppress longLineCheck
667
668      //////////////////////////////////////////////////////////////////////////
669      // RenderThread
670      //////////////////////////////////////////////////////////////////////////
671      registerEventInfo({
672          title: 'DrawFrame',
673          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
674      registerEventInfo({
675          title: 'doFrame',
676          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
677      registerEventInfo({
678          title: 'syncFrameState',
679          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
680      registerEventInfo({
681          title: 'flush drawing commands',
682          description: 'Issuing the now complete drawing commands to the GPU.'}); // @suppress longLineCheck
683      registerEventInfo({
684          title: 'eglSwapBuffers',
685          description: 'Complete GPU rendering of the frame.'}); // @suppress longLineCheck
686
687      //////////////////////////////////////////////////////////////////////////
688      // RecyclerView
689      //////////////////////////////////////////////////////////////////////////
690      registerEventInfo({
691          title: 'RV Scroll',
692          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
693      registerEventInfo({
694          title: 'RV OnLayout',
695          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
696      registerEventInfo({
697          title: 'RV FullInvalidate',
698          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
699      registerEventInfo({
700          title: 'RV PartialInvalidate',
701          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
702      registerEventInfo({
703          title: 'RV OnBindView',
704          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
705      registerEventInfo({
706          title: 'RV CreateView',
707          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
708
709      //////////////////////////////////////////////////////////////////////////
710      // Graphics + Composition
711      //////////////////////////////////////////////////////////////////////////
712      // TODO(ccraik): SurfaceFlinger work
713      registerEventInfo({
714          title: 'eglSwapBuffers',
715          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
716    },
717
718    applyEventInfosRecursive_: function(parentNames, slice) {
719      var checkExpectedParentNames = function(expectedParentNames) {
720        if (!expectedParentNames)
721          return true;
722        return expectedParentNames.some(function(name) {
723          return name in parentNames;
724        });
725      };
726
727
728      // Set EventInfo on the slice if it matches title, and parent.
729      if (slice.title in this.titleInfoLookup) {
730        if (checkExpectedParentNames(this.titleParentLookup[slice.title]))
731          slice.info = this.titleInfoLookup[slice.title];
732      }
733
734      // Push slice into parentNames, and recurse over subSlices.
735      if (slice.subSlices.length > 0) {
736        // Increment title in parentName dict.
737        if (!(slice.title in parentNames))
738          parentNames[slice.title] = 0;
739        parentNames[slice.title]++;
740
741        // Recurse over subSlices.
742        slice.subSlices.forEach(function(subSlice) {
743          this.applyEventInfosRecursive_(parentNames, subSlice);
744        }, this);
745
746        // Decrement title in parentName dict.
747        parentNames[slice.title]--;
748        if (parentNames[slice.title] == 0)
749          delete parentNames[slice.title];
750      }
751    },
752
753    applyEventInfos: function(sliceGroup) {
754      sliceGroup.topLevelSlices.forEach(function(slice) {
755        this.applyEventInfosRecursive_({}, slice);
756      }, this);
757    }
758  };
759
760  return {
761    AndroidAuditor: AndroidAuditor
762  };
763});
764</script>
765