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