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<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> 8<link rel="import" href="/tracing/model/async_slice.html"> 9<link rel="import" href="/tracing/model/event_set.html"> 10 11<script> 12'use strict'; 13 14tr.exportTo('tr.e.cc', function() { 15 var AsyncSlice = tr.model.AsyncSlice; 16 var EventSet = tr.model.EventSet; 17 18 var UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT'; 19 var ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT'; 20 var BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT'; 21 var END_COMP_NAME = 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT'; 22 23 var MAIN_RENDERER_THREAD_NAME = 'CrRendererMain'; 24 var COMPOSITOR_THREAD_NAME = 'Compositor'; 25 26 var POSTTASK_FLOW_EVENT = 'disabled-by-default-toplevel.flow'; 27 var IPC_FLOW_EVENT = 'disabled-by-default-ipc.flow'; 28 29 var INPUT_EVENT_TYPE_NAMES = { 30 CHAR: 'Char', 31 CLICK: 'GestureClick', 32 CONTEXT_MENU: 'ContextMenu', 33 FLING_CANCEL: 'GestureFlingCancel', 34 FLING_START: 'GestureFlingStart', 35 KEY_DOWN: 'KeyDown', 36 KEY_DOWN_RAW: 'RawKeyDown', 37 KEY_UP: 'KeyUp', 38 LATENCY_SCROLL_UPDATE: 'ScrollUpdate', 39 MOUSE_DOWN: 'MouseDown', 40 MOUSE_ENTER: 'MouseEnter', 41 MOUSE_LEAVE: 'MouseLeave', 42 MOUSE_MOVE: 'MouseMove', 43 MOUSE_UP: 'MouseUp', 44 MOUSE_WHEEL: 'MouseWheel', 45 PINCH_BEGIN: 'GesturePinchBegin', 46 PINCH_END: 'GesturePinchEnd', 47 PINCH_UPDATE: 'GesturePinchUpdate', 48 SCROLL_BEGIN: 'GestureScrollBegin', 49 SCROLL_END: 'GestureScrollEnd', 50 SCROLL_UPDATE: 'GestureScrollUpdate', 51 SCROLL_UPDATE_RENDERER: 'ScrollUpdate', 52 SHOW_PRESS: 'GestureShowPress', 53 TAP: 'GestureTap', 54 TAP_CANCEL: 'GestureTapCancel', 55 TAP_DOWN: 'GestureTapDown', 56 TOUCH_CANCEL: 'TouchCancel', 57 TOUCH_END: 'TouchEnd', 58 TOUCH_MOVE: 'TouchMove', 59 TOUCH_START: 'TouchStart', 60 UNKNOWN: 'UNKNOWN' 61 }; 62 63 function InputLatencyAsyncSlice() { 64 AsyncSlice.apply(this, arguments); 65 this.associatedEvents_ = new EventSet(); 66 this.typeName_ = undefined; 67 if (!this.isLegacyEvent) 68 this.determineModernTypeName_(); 69 } 70 71 InputLatencyAsyncSlice.prototype = { 72 __proto__: AsyncSlice.prototype, 73 74 // Legacy InputLatencyAsyncSlices involve a top-level slice titled 75 // "InputLatency" containing a subSlice whose title starts with 76 // "InputLatency:". Modern InputLatencyAsyncSlices involve a single 77 // top-level slice whose title starts with "InputLatency::". 78 // Legacy subSlices are not available at construction time, so 79 // determineLegacyTypeName_() must be called at get time. 80 // So this returns false for the legacy subSlice events titled like 81 // "InputLatency:Foo" even though they are technically legacy events. 82 get isLegacyEvent() { 83 return this.title === 'InputLatency'; 84 }, 85 86 get typeName() { 87 if (!this.typeName_) 88 this.determineLegacyTypeName_(); 89 return this.typeName_; 90 }, 91 92 checkTypeName_: function() { 93 if (!this.typeName_) 94 throw 'Unable to determine typeName'; 95 var found = false; 96 for (var type_name in INPUT_EVENT_TYPE_NAMES) { 97 if (this.typeName === INPUT_EVENT_TYPE_NAMES[type_name]) { 98 found = true; 99 break; 100 } 101 } 102 if (!found) 103 this.typeName_ = INPUT_EVENT_TYPE_NAMES.UNKNOWN; 104 }, 105 106 determineModernTypeName_: function() { 107 // This method works both on modern events titled like 108 // "InputLatency::Foo" and also on the legacy subSlices titled like 109 // "InputLatency:Foo". Modern events' titles contain 2 colons, whereas the 110 // legacy subSlices events contain 1 colon. 111 112 var lastColonIndex = this.title.lastIndexOf(':'); 113 if (lastColonIndex < 0) 114 return; 115 116 var characterAfterLastColonIndex = lastColonIndex + 1; 117 this.typeName_ = this.title.slice(characterAfterLastColonIndex); 118 119 // Check that the determined typeName is known. 120 this.checkTypeName_(); 121 }, 122 123 determineLegacyTypeName_: function() { 124 // Iterate over all descendent subSlices. 125 this.iterateAllDescendents(function(subSlice) { 126 127 // If |subSlice| is not an InputLatencyAsyncSlice, then ignore it. 128 var subSliceIsAInputLatencyAsyncSlice = ( 129 subSlice instanceof InputLatencyAsyncSlice); 130 if (!subSliceIsAInputLatencyAsyncSlice) 131 return; 132 133 // If |subSlice| does not have a typeName, then ignore it. 134 if (!subSlice.typeName) 135 return; 136 137 // If |this| already has a typeName and |subSlice| has a different 138 // typeName, then explode! 139 if (this.typeName_ && subSlice.typeName_) { 140 var subSliceHasDifferentTypeName = ( 141 this.typeName_ !== subSlice.typeName_); 142 if (subSliceHasDifferentTypeName) { 143 throw 'InputLatencyAsyncSlice.determineLegacyTypeName_() ' + 144 ' found multiple typeNames'; 145 } 146 } 147 148 // The typeName of |this| top-level event is whatever the typeName of 149 // |subSlice| is. Set |this.typeName_| to the subSlice's typeName. 150 this.typeName_ = subSlice.typeName_; 151 }, this); 152 153 // If typeName could not be determined, then explode! 154 if (!this.typeName_) 155 throw 'InputLatencyAsyncSlice.determineLegacyTypeName_() failed'; 156 157 // Check that the determined typeName is known. 158 this.checkTypeName_(); 159 }, 160 161 getRendererHelper: function(sourceSlices) { 162 var traceModel = this.startThread.parent.model; 163 var modelHelper = traceModel.getOrCreateHelper( 164 tr.model.helpers.ChromeModelHelper); 165 if (!modelHelper) 166 return undefined; 167 168 var mainThread = undefined; 169 var compositorThread = undefined; 170 171 for (var i in sourceSlices) { 172 if (sourceSlices[i].parentContainer.name === 173 MAIN_RENDERER_THREAD_NAME) 174 mainThread = sourceSlices[i].parentContainer; 175 else if (sourceSlices[i].parentContainer.name === 176 COMPOSITOR_THREAD_NAME) 177 compositorThread = sourceSlices[i].parentContainer; 178 179 if (mainThread && compositorThread) 180 break; 181 } 182 183 var rendererHelpers = modelHelper.rendererHelpers; 184 185 var pids = Object.keys(rendererHelpers); 186 for (var i = 0; i < pids.length; i++) { 187 var pid = pids[i]; 188 var rendererHelper = rendererHelpers[pid]; 189 if (rendererHelper.mainThread === mainThread || 190 rendererHelper.compositorThread === compositorThread) 191 return rendererHelper; 192 } 193 194 return undefined; 195 }, 196 197 addEntireSliceHierarchy: function(slice) { 198 this.associatedEvents_.push(slice); 199 slice.iterateAllSubsequentSlices(function(subsequentSlice) { 200 this.associatedEvents_.push(subsequentSlice); 201 }, this); 202 }, 203 204 addDirectlyAssociatedEvents: function(flowEvents) { 205 var slices = []; 206 207 flowEvents.forEach(function(flowEvent) { 208 this.associatedEvents_.push(flowEvent); 209 var newSource = flowEvent.startSlice.mostTopLevelSlice; 210 if (slices.indexOf(newSource) === -1) 211 slices.push(newSource); 212 }, this); 213 214 var lastFlowEvent = flowEvents[flowEvents.length - 1]; 215 var lastSource = lastFlowEvent.endSlice.mostTopLevelSlice; 216 if (slices.indexOf(lastSource) === -1) 217 slices.push(lastSource); 218 219 return slices; 220 }, 221 222 // Find the Latency::ScrollUpdate slice that corresponds to the 223 // InputLatency::GestureScrollUpdate slice. 224 // The C++ CL that makes this connection is at: 225 // https://codereview.chromium.org/1178963003 226 addScrollUpdateEvents: function(rendererHelper) { 227 if (!rendererHelper || !rendererHelper.compositorThread) 228 return; 229 230 var compositorThread = rendererHelper.compositorThread; 231 var gestureScrollUpdateStart = this.start; 232 var gestureScrollUpdateEnd = this.end; 233 234 var allCompositorAsyncSlices = 235 compositorThread.asyncSliceGroup.slices; 236 237 for (var i in allCompositorAsyncSlices) { 238 var slice = allCompositorAsyncSlices[i]; 239 240 if (slice.title !== 'Latency::ScrollUpdate') 241 continue; 242 243 var parentId = slice.args.data. 244 INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT. 245 sequence_number; 246 247 if (parentId === undefined) { 248 // Old trace, we can only rely on the timestamp to find the slice 249 if (slice.start < gestureScrollUpdateStart || 250 slice.start >= gestureScrollUpdateEnd) 251 continue; 252 } else { 253 // New trace, we can definitively find the latency slice by comparing 254 // its sequence number with gesture id 255 if (parseInt(parentId) !== parseInt(this.id)) 256 continue; 257 } 258 259 slice.associatedEvents.forEach(function(event) { 260 this.associatedEvents_.push(event); 261 }, this); 262 break; 263 } 264 }, 265 266 // Return true if the slice hierarchy is tracked by LatencyInfo of other 267 // input latency events. If the slice hierarchy is tracked by both, this 268 // function still returns true. 269 belongToOtherInputs: function(slice, flowEvents) { 270 var fromOtherInputs = false; 271 272 slice.iterateEntireHierarchy(function(subsequentSlice) { 273 if (fromOtherInputs) 274 return; 275 276 subsequentSlice.inFlowEvents.forEach(function(inflow) { 277 if (fromOtherInputs) 278 return; 279 280 if (inflow.category.indexOf('input') > -1) { 281 if (flowEvents.indexOf(inflow) === -1) 282 fromOtherInputs = true; 283 } 284 }, this); 285 }, this); 286 287 return fromOtherInputs; 288 }, 289 290 // Return true if |event| triggers slices of other inputs. 291 triggerOtherInputs: function(event, flowEvents) { 292 if (event.outFlowEvents === undefined || 293 event.outFlowEvents.length === 0) 294 return false; 295 296 // Once we fix the bug of flow event binding, there should exist one and 297 // only one outgoing flow (PostTask) from ScheduleBeginImplFrameDeadline 298 // and PostComposite. 299 var flow = event.outFlowEvents[0]; 300 301 if (flow.category !== POSTTASK_FLOW_EVENT || 302 !flow.endSlice) 303 return false; 304 305 var endSlice = flow.endSlice; 306 if (this.belongToOtherInputs(endSlice.mostTopLevelSlice, flowEvents)) 307 return true; 308 309 return false; 310 }, 311 312 // Follow outgoing flow of subsequentSlices in the current hierarchy. 313 // We also handle cases where different inputs interfere with each other. 314 followSubsequentSlices: function(event, queue, visited, flowEvents) { 315 var stopFollowing = false; 316 var inputAck = false; 317 318 event.iterateAllSubsequentSlices(function(slice) { 319 if (stopFollowing) 320 return; 321 322 // Do not follow TaskQueueManager::RunTask because it causes 323 // many false events to be included. 324 if (slice.title === 'TaskQueueManager::RunTask') 325 return; 326 327 // Do not follow ScheduledActionSendBeginMainFrame because the real 328 // main thread BeginMainFrame is already traced by LatencyInfo flow. 329 if (slice.title === 'ThreadProxy::ScheduledActionSendBeginMainFrame') 330 return; 331 332 // Do not follow ScheduleBeginImplFrameDeadline that triggers an 333 // OnBeginImplFrameDeadline that is tracked by another LatencyInfo. 334 if (slice.title === 'Scheduler::ScheduleBeginImplFrameDeadline') { 335 if (this.triggerOtherInputs(slice, flowEvents)) 336 return; 337 } 338 339 // Do not follow PostComposite that triggers CompositeImmediately 340 // that is tracked by another LatencyInfo. 341 if (slice.title === 'CompositorImpl::PostComposite') { 342 if (this.triggerOtherInputs(slice, flowEvents)) 343 return; 344 } 345 346 // Stop following the rest of the current slice hierarchy if 347 // FilterAndSendWebInputEvent occurs after ProcessInputEventAck. 348 if (slice.title === 'InputRouterImpl::ProcessInputEventAck') 349 inputAck = true; 350 if (inputAck && 351 slice.title === 'InputRouterImpl::FilterAndSendWebInputEvent') 352 stopFollowing = true; 353 354 this.followCurrentSlice(slice, queue, visited); 355 }, this); 356 }, 357 358 // Follow outgoing flow events of the current slice. 359 followCurrentSlice: function(event, queue, visited) { 360 event.outFlowEvents.forEach(function(outflow) { 361 if ((outflow.category === POSTTASK_FLOW_EVENT || 362 outflow.category === IPC_FLOW_EVENT) && 363 outflow.endSlice) { 364 this.associatedEvents_.push(outflow); 365 366 var nextEvent = outflow.endSlice.mostTopLevelSlice; 367 if (!visited.contains(nextEvent)) { 368 visited.push(nextEvent); 369 queue.push(nextEvent); 370 } 371 } 372 }, this); 373 }, 374 375 backtraceFromDraw: function(beginImplFrame, visited) { 376 var pendingEventQueue = []; 377 pendingEventQueue.push(beginImplFrame.mostTopLevelSlice); 378 379 while (pendingEventQueue.length !== 0) { 380 var event = pendingEventQueue.pop(); 381 382 this.addEntireSliceHierarchy(event); 383 384 // TODO(yuhao): For now, we backtrace all the way to the source input. 385 // But is this really needed? I will have an entry in the design 386 // doc to discuss this. 387 event.inFlowEvents.forEach(function(inflow) { 388 if (inflow.category === POSTTASK_FLOW_EVENT && inflow.startSlice) { 389 var nextEvent = inflow.startSlice.mostTopLevelSlice; 390 if (!visited.contains(nextEvent)) { 391 visited.push(nextEvent); 392 pendingEventQueue.push(nextEvent); 393 } 394 } 395 }, this); 396 } 397 }, 398 399 sortRasterizerSlices: function(rasterWorkerThreads, 400 sortedRasterizerSlices) { 401 rasterWorkerThreads.forEach(function(rasterizer) { 402 Array.prototype.push.apply(sortedRasterizerSlices, 403 rasterizer.sliceGroup.slices); 404 }, this); 405 406 sortedRasterizerSlices.sort(function(a, b) { 407 if (a.start !== b.start) 408 return a.start - b.start; 409 return a.guid - b.guid; 410 }); 411 }, 412 413 // Find rasterization slices that have the source_prepare_tiles_id 414 // same as the prepare_tiles_id of TileManager::PrepareTiles 415 // The C++ CL that makes this connection is at: 416 // https://codereview.chromium.org/1208683002/ 417 addRasterizationEvents: function(prepareTiles, rendererHelper, 418 visited, flowEvents, sortedRasterizerSlices) { 419 if (!prepareTiles.args.prepare_tiles_id) 420 return; 421 422 if (!rendererHelper || !rendererHelper.rasterWorkerThreads) 423 return; 424 425 var rasterWorkerThreads = rendererHelper.rasterWorkerThreads; 426 var prepare_tile_id = prepareTiles.args.prepare_tiles_id; 427 var pendingEventQueue = []; 428 429 // Collect all the rasterizer tasks. Return the cached copy if possible. 430 if (sortedRasterizerSlices.length === 0) 431 this.sortRasterizerSlices(rasterWorkerThreads, sortedRasterizerSlices); 432 433 // TODO(yuhao): Once TaskSetFinishedTaskImpl also get the prepare_tile_id 434 // we can simply track by checking id rather than counting. 435 var numFinishedTasks = 0; 436 var RASTER_TASK_TITLE = 'RasterizerTaskImpl::RunOnWorkerThread'; 437 var IMAGEDECODE_TASK_TITLE = 'ImageDecodeTaskImpl::RunOnWorkerThread'; 438 var FINISHED_TASK_TITLE = 'TaskSetFinishedTaskImpl::RunOnWorkerThread'; 439 440 for (var i = 0; i < sortedRasterizerSlices.length; i++) { 441 var task = sortedRasterizerSlices[i]; 442 443 if (task.title === RASTER_TASK_TITLE || 444 task.title === IMAGEDECODE_TASK_TITLE) { 445 if (task.args.source_prepare_tiles_id === prepare_tile_id) 446 this.addEntireSliceHierarchy(task.mostTopLevelSlice); 447 } else if (task.title === FINISHED_TASK_TITLE) { 448 if (task.start > prepareTiles.start) { 449 pendingEventQueue.push(task.mostTopLevelSlice); 450 if (++numFinishedTasks === 3) 451 break; 452 } 453 } 454 } 455 456 // Trace PostTask from rasterizer tasks. 457 while (pendingEventQueue.length != 0) { 458 var event = pendingEventQueue.pop(); 459 460 this.addEntireSliceHierarchy(event); 461 this.followSubsequentSlices(event, pendingEventQueue, visited, 462 flowEvents); 463 } 464 }, 465 466 addOtherCausallyRelatedEvents: function(rendererHelper, sourceSlices, 467 flowEvents, sortedRasterizerSlices) { 468 var pendingEventQueue = []; 469 // Keep track of visited nodes when traversing a DAG 470 var visitedEvents = new EventSet(); 471 var beginImplFrame = undefined; 472 var prepareTiles = undefined; 473 var sortedRasterizerSlices = []; 474 475 sourceSlices.forEach(function(sourceSlice) { 476 if (!visitedEvents.contains(sourceSlice)) { 477 visitedEvents.push(sourceSlice); 478 pendingEventQueue.push(sourceSlice); 479 } 480 }, this); 481 482 while (pendingEventQueue.length != 0) { 483 var event = pendingEventQueue.pop(); 484 485 // Push the current event chunk into associatedEvents. 486 this.addEntireSliceHierarchy(event); 487 488 this.followCurrentSlice(event, pendingEventQueue, visitedEvents); 489 490 this.followSubsequentSlices(event, pendingEventQueue, visitedEvents, 491 flowEvents); 492 493 // The rasterization work (CompositorTileWorker thread) and the 494 // Compositor tile manager are connect by the prepare_tiles_id 495 // instead of flow events. 496 var COMPOSITOR_PREPARE_TILES = 'TileManager::PrepareTiles'; 497 prepareTiles = event.findDescendentSlice(COMPOSITOR_PREPARE_TILES); 498 if (prepareTiles) 499 this.addRasterizationEvents(prepareTiles, rendererHelper, 500 visitedEvents, flowEvents, sortedRasterizerSlices); 501 502 // OnBeginImplFrameDeadline could be triggered by other inputs. 503 // For now, we backtrace from it. 504 // TODO(yuhao): There are more such slices that we need to backtrace 505 var COMPOSITOR_ON_BIFD = 'Scheduler::OnBeginImplFrameDeadline'; 506 beginImplFrame = event.findDescendentSlice(COMPOSITOR_ON_BIFD); 507 if (beginImplFrame) 508 this.backtraceFromDraw(beginImplFrame, visitedEvents); 509 } 510 511 // A separate pass on GestureScrollUpdate. 512 // Scroll update doesn't go through the main thread, but the compositor 513 // may go back to the main thread if there is an onscroll event handler. 514 // This is captured by a different flow event, which does not have the 515 // same ID as the Input Latency Event, but it is technically causally 516 // related to the GestureScrollUpdate input. Add them manually for now. 517 var INPUT_GSU = 'InputLatency::GestureScrollUpdate'; 518 if (this.title === INPUT_GSU) 519 this.addScrollUpdateEvents(rendererHelper); 520 }, 521 522 get associatedEvents() { 523 if (this.associatedEvents_.length !== 0) 524 return this.associatedEvents_; 525 526 var modelIndices = this.startThread.parent.model.modelIndices; 527 var flowEvents = modelIndices.getFlowEventsWithId(this.id); 528 529 if (flowEvents.length === 0) 530 return this.associatedEvents_; 531 532 // Step 1: Get events that are directly connected by the LatencyInfo 533 // flow events. This gives us a small set of events that are guaranteed 534 // to be associated with the input, but are almost certain incomplete. 535 // We call this set "source" event set. 536 // This step returns the "source" event set (sourceSlices), which is then 537 // used in the second step. 538 var sourceSlices = this.addDirectlyAssociatedEvents(flowEvents); 539 540 // Step 2: Start from the previously constructed "source" event set, we 541 // follow the toplevel (i.e., PostTask) and IPC flow events. Any slices 542 // that are reachable from the "source" event set via PostTasks or IPCs 543 // are conservatively considered associated with the input event. 544 // We then deal with specific cases where flow events either over include 545 // or miss capturing slices. 546 var rendererHelper = this.getRendererHelper(sourceSlices); 547 this.addOtherCausallyRelatedEvents(rendererHelper, sourceSlices, 548 flowEvents); 549 550 return this.associatedEvents_; 551 }, 552 553 get inputLatency() { 554 if (!('data' in this.args)) 555 return undefined; 556 557 var data = this.args.data; 558 if (!(END_COMP_NAME in data)) 559 return undefined; 560 561 var latency = 0; 562 var endTime = data[END_COMP_NAME].time; 563 if (ORIGINAL_COMP_NAME in data) { 564 latency = endTime - data[ORIGINAL_COMP_NAME].time; 565 } else if (UI_COMP_NAME in data) { 566 latency = endTime - data[UI_COMP_NAME].time; 567 } else if (BEGIN_COMP_NAME in data) { 568 latency = endTime - data[BEGIN_COMP_NAME].time; 569 } else { 570 throw new Error('No valid begin latency component'); 571 } 572 return latency; 573 } 574 }; 575 576 var eventTypeNames = [ 577 'Char', 578 'ContextMenu', 579 'GestureClick', 580 'GestureFlingCancel', 581 'GestureFlingStart', 582 'GestureScrollBegin', 583 'GestureScrollEnd', 584 'GestureScrollUpdate', 585 'GestureShowPress', 586 'GestureTap', 587 'GestureTapCancel', 588 'GestureTapDown', 589 'GesturePinchBegin', 590 'GesturePinchEnd', 591 'GesturePinchUpdate', 592 'KeyDown', 593 'KeyUp', 594 'MouseDown', 595 'MouseEnter', 596 'MouseLeave', 597 'MouseMove', 598 'MouseUp', 599 'MouseWheel', 600 'RawKeyDown', 601 'ScrollUpdate', 602 'TouchCancel', 603 'TouchEnd', 604 'TouchMove', 605 'TouchStart' 606 ]; 607 var allTypeNames = ['InputLatency']; 608 eventTypeNames.forEach(function(eventTypeName) { 609 // Old style. 610 allTypeNames.push('InputLatency:' + eventTypeName); 611 612 // New style. 613 allTypeNames.push('InputLatency::' + eventTypeName); 614 }); 615 616 AsyncSlice.register( 617 InputLatencyAsyncSlice, 618 { 619 typeNames: allTypeNames, 620 categoryParts: ['latencyInfo'] 621 }); 622 623 return { 624 InputLatencyAsyncSlice: InputLatencyAsyncSlice, 625 INPUT_EVENT_TYPE_NAMES: INPUT_EVENT_TYPE_NAMES 626 }; 627}); 628</script> 629