Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 1 | <!DOCTYPE html> |
| 2 | <!-- |
| 3 | Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 4 | Use of this source code is governed by a BSD-style license that can be |
| 5 | found in the LICENSE file. |
| 6 | --> |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 7 | <link rel="import" href="/base/iteration_helpers.html"> |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 8 | <link rel="import" href="/base/statistics.html"> |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 9 | <link rel="import" href="/base/time.html"> |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 10 | <link rel="import" href="/core/auditor.html"> |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 11 | <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 Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 14 | <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 | */ |
| 23 | tv.exportTo('tv.e.audits', function() { |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 24 | var SCHEDULING_STATE = tv.c.trace_model.SCHEDULING_STATE; |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 25 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 29 | var InteractionRecord = tv.c.trace_model.InteractionRecord; |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 30 | var Alert = tv.c.trace_model.Alert; |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 31 | var EventInfo = tv.c.trace_model.EventInfo; |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 32 | var TimeDuration = tv.b.TimeDuration; |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 33 | |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 34 | // TODO: extract from VSYNC, since not all devices have vsync near 60fps |
| 35 | var EXPECTED_FRAME_TIME_MS = 16.67; |
| 36 | |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 37 | function getStart(e) { return e.start; } |
| 38 | function getDuration(e) { return e.duration; } |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 39 | // 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 43 | |
| 44 | function frameIsActivityStart(frame) { |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 45 | for (var i = 0; i < frame.associatedEvents.length; i++) { |
| 46 | if (frame.associatedEvents[i].title == 'activityStart') |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 47 | 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 Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 55 | var Auditor = tv.c.Auditor; |
| 56 | var AndroidModelHelper = tv.e.audits.AndroidModelHelper; |
| 57 | |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 58 | function frameMissedDeadline(frame) { |
| 59 | return frame.args['deadline'] && frame.args['deadline'] < frame.end; |
| 60 | } |
| 61 | |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 62 | /** |
| 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 73 | ////////////////////////////////////////////////////////////////////////////// |
| 74 | // Rendering / RenderThread alerts - only available on SDK 22+ |
| 75 | ////////////////////////////////////////////////////////////////////////////// |
| 76 | |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 77 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 87 | /^(.+) alpha caused (unclipped )?saveLayer (\d+)x(\d+)$/; |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 88 | var saveLayerRegEx = /^(unclipped )?saveLayer (\d+)x(\d+)$/; |
| 89 | |
| 90 | var ret = []; |
| 91 | var events = []; |
| 92 | |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 93 | frame.associatedEvents.forEach(function(slice) { |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 94 | var match = badAlphaRegEx.exec(slice.title); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 95 | if (match) { |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 96 | // due to bug in tracing code on SDK 22, ignore |
| 97 | // presence of 'unclipped' string in View alpha slices |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 98 | var args = { 'view name': match[1], |
| 99 | width: parseInt(match[3]), |
| 100 | height: parseInt(match[4]) }; |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 101 | ret.push(new Alert(AndroidAuditor.viewAlphaAlertInfo_, |
| 102 | slice.start, [slice], args)); |
| 103 | } else if (saveLayerRegEx.test(slice.title)) |
| 104 | events.push(slice); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 105 | }, this); |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 106 | |
| 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 119 | 'Unclipped saveLayer count (especially bad!)': unclippedSeen, |
| 120 | 'Clipped saveLayer count': clippedSeen |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 121 | }; |
| 122 | |
| 123 | events.push(frame); |
| 124 | ret.push(new Alert(AndroidAuditor.saveLayerAlertInfo_, |
| 125 | earliestStart, events, args)); |
| 126 | } |
| 127 | |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 128 | return ret; |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 129 | }; |
| 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 138 | var events = frame.associatedEvents.filter(function(event) { |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 139 | 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 149 | { 'Time spent': new TimeDuration(duration) }); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 150 | } |
| 151 | |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 152 | |
| 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 156 | 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 163 | frame.associatedEvents.forEach(function(event) { |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 164 | 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 175 | var mPixels = (pixelsUploaded / 1000000).toFixed(2) + ' million'; |
| 176 | var args = { 'Pixels uploaded': mPixels, |
| 177 | 'Time spent': new TimeDuration(duration) }; |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 178 | events.push(frame); |
| 179 | return new Alert(AndroidAuditor.uploadAlertInfo_, start, events, args); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 180 | } |
| 181 | |
| 182 | ////////////////////////////////////////////////////////////////////////////// |
| 183 | // UI responsiveness alerts |
| 184 | ////////////////////////////////////////////////////////////////////////////// |
| 185 | |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 186 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 192 | AndroidAuditor.getListViewAlert_ = function(frame) { |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 193 | var events = frame.associatedEvents.filter(function(event) { |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 194 | return event.title == 'obtainView' || event.title == 'setupListItem'; |
| 195 | }); |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 196 | var duration = Statistics.sum(events, getCpuDuration); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 197 | |
| 198 | if (events.length == 0 || duration < 3) |
| 199 | return undefined; |
| 200 | |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 201 | // 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 212 | var start = Statistics.min(events, getStart); |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 213 | 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 218 | events.push(frame); |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 219 | return new Alert(eventInfo, start, events, args); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 220 | } |
| 221 | |
| 222 | |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 223 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 226 | AndroidAuditor.getMeasureLayoutAlert_ = function(frame) { |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 227 | var events = frame.associatedEvents.filter(function(event) { |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 228 | return event.title == 'measure' || event.title == 'layout'; |
| 229 | }); |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 230 | var duration = Statistics.sum(events, getCpuDuration); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 231 | |
| 232 | if (events.length == 0 || duration < 3) |
| 233 | return undefined; |
| 234 | |
| 235 | var start = Statistics.min(events, getStart); |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 236 | events.push(frame); |
| 237 | return new Alert(AndroidAuditor.measureLayoutAlertInfo_, start, events, |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 238 | { 'Time spent': new TimeDuration(duration) }); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 239 | } |
| 240 | |
| 241 | |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 242 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 245 | AndroidAuditor.getViewDrawAlert_ = function(frame) { |
| 246 | var slice = undefined; |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 247 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 251 | break; |
| 252 | } |
| 253 | } |
| 254 | |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 255 | if (!slice || getCpuDuration(slice) < 3) |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 256 | return undefined; |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 257 | return new Alert(AndroidAuditor.viewDrawAlertInfo_, slice.start, |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 258 | [slice, frame], |
| 259 | { 'Time spent': new TimeDuration(getCpuDuration(slice)) }); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 260 | } |
| 261 | |
| 262 | |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 263 | ////////////////////////////////////////////////////////////////////////////// |
| 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 271 | var events = frame.associatedEvents.filter(function(event) { |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 272 | 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 282 | { 'Blocked duration': new TimeDuration(blockedDuration) }); |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 283 | }; |
| 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 290 | var events = frame.associatedEvents.filter(function(event) { |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 291 | 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 301 | { 'Blocked duration': new TimeDuration(blockedDuration) }); |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 302 | }; |
| 303 | |
Chris Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 304 | 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 339 | |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 340 | AndroidAuditor.prototype = { |
| 341 | __proto__: Auditor.prototype, |
| 342 | |
| 343 | renameAndSort_: function() { |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 344 | this.model.kernel.important = false;// auto collapse |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 345 | // 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 352 | process.important = false; // auto collapse |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 353 | 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 383 | var surfaceFlinger = this.helper.surfaceFlinger; |
| 384 | |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 385 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 390 | if (frame.totalDuration > EXPECTED_FRAME_TIME_MS * 2) { |
| 391 | badFramesObserved += 2; |
| 392 | frame.perfClass = FRAME_PERF_CLASS.TERRIBLE; |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 393 | } else if (frame.totalDuration > EXPECTED_FRAME_TIME_MS || |
| 394 | frameMissedDeadline(frame)) { |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 395 | badFramesObserved++; |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 396 | frame.perfClass = FRAME_PERF_CLASS.BAD; |
| 397 | } else { |
| 398 | frame.perfClass = FRAME_PERF_CLASS.GOOD; |
| 399 | } |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 400 | }); |
| 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 415 | 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 Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 425 | runAnnotate: function() { |
| 426 | if (!this.helper) |
| 427 | return; |
| 428 | |
| 429 | this.renameAndSort_(); |
| 430 | this.pushFramesAndJudgeJank_(); |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 431 | this.pushEventInfo_(); |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 432 | |
| 433 | this.helper.iterateImportantSlices(function(slice) { |
| 434 | slice.important = true; |
| 435 | }); |
| 436 | }, |
| 437 | |
| 438 | runAudit: function() { |
| 439 | if (!this.helper) |
| 440 | return; |
| 441 | |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 442 | var alerts = this.model.alerts; |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 443 | this.helper.apps.forEach(function(app) { |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 444 | app.getFrames().forEach(function(frame) { |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 445 | alerts.push.apply(alerts, AndroidAuditor.getSaveLayerAlerts_(frame)); |
| 446 | |
| 447 | // skip most alerts for neutral or good frames |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 448 | if (frame.perfClass == FRAME_PERF_CLASS.NEUTRAL || |
| 449 | frame.perfClass == FRAME_PERF_CLASS.GOOD) |
| 450 | return; |
| 451 | |
Chris Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 452 | var alert = AndroidAuditor.getPathAlert_(frame); |
| 453 | if (alert) |
| 454 | alerts.push(alert); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 455 | 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 467 | 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 473 | var alert = AndroidAuditor.getSchedulingAlert_(frame); |
| 474 | if (alert) |
| 475 | alerts.push(alert); |
Chris Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 476 | }); |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 477 | }, this); |
| 478 | |
| 479 | this.addRenderingInteractionRecords(); |
| 480 | this.addInputInteractionRecords(); |
| 481 | }, |
| 482 | |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 483 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 491 | var ir = new InteractionRecord('Rendering', |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 492 | 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 Craik | beca7ae | 2015-04-07 13:29:55 -0700 | [diff] [blame] | 507 | var ir = new InteractionRecord('Input', |
Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 508 | 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 521 | 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 536 | 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 540 | ////////////////////////////////////////////////////////////////////////// |
| 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 Craik | 44c2820 | 2015-05-12 17:25:16 -0700 | [diff] [blame^] | 608 | |
| 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 Craik | 1983215 | 2015-04-16 15:43:38 -0700 | [diff] [blame] | 638 | }, |
| 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 Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 674 | return { |
| 675 | AndroidAuditor: AndroidAuditor |
| 676 | }; |
| 677 | }); |
| 678 | </script> |