1<!DOCTYPE html> 2<!-- 3Copyright (c) 2015 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/range_utils.html"> 9<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html"> 10<link rel="import" href="/tracing/importer/proto_expectation.html"> 11 12<script> 13'use strict'; 14 15tr.exportTo('tr.importer', function() { 16 var ProtoExpectation = tr.importer.ProtoExpectation; 17 18 var INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES; 19 20 var KEYBOARD_TYPE_NAMES = [ 21 INPUT_TYPE.CHAR, 22 INPUT_TYPE.KEY_DOWN_RAW, 23 INPUT_TYPE.KEY_DOWN, 24 INPUT_TYPE.KEY_UP 25 ]; 26 var MOUSE_RESPONSE_TYPE_NAMES = [ 27 INPUT_TYPE.CLICK, 28 INPUT_TYPE.CONTEXT_MENU 29 ]; 30 var MOUSE_WHEEL_TYPE_NAMES = [ 31 INPUT_TYPE.MOUSE_WHEEL 32 ]; 33 var MOUSE_DRAG_TYPE_NAMES = [ 34 INPUT_TYPE.MOUSE_DOWN, 35 INPUT_TYPE.MOUSE_MOVE, 36 INPUT_TYPE.MOUSE_UP 37 ]; 38 var TAP_TYPE_NAMES = [ 39 INPUT_TYPE.TAP, 40 INPUT_TYPE.TAP_CANCEL, 41 INPUT_TYPE.TAP_DOWN 42 ]; 43 var PINCH_TYPE_NAMES = [ 44 INPUT_TYPE.PINCH_BEGIN, 45 INPUT_TYPE.PINCH_END, 46 INPUT_TYPE.PINCH_UPDATE 47 ]; 48 var FLING_TYPE_NAMES = [ 49 INPUT_TYPE.FLING_CANCEL, 50 INPUT_TYPE.FLING_START 51 ]; 52 var TOUCH_TYPE_NAMES = [ 53 INPUT_TYPE.TOUCH_END, 54 INPUT_TYPE.TOUCH_MOVE, 55 INPUT_TYPE.TOUCH_START 56 ]; 57 var SCROLL_TYPE_NAMES = [ 58 INPUT_TYPE.SCROLL_BEGIN, 59 INPUT_TYPE.SCROLL_END, 60 INPUT_TYPE.SCROLL_UPDATE 61 ]; 62 var ALL_HANDLED_TYPE_NAMES = [].concat( 63 KEYBOARD_TYPE_NAMES, 64 MOUSE_RESPONSE_TYPE_NAMES, 65 MOUSE_WHEEL_TYPE_NAMES, 66 MOUSE_DRAG_TYPE_NAMES, 67 PINCH_TYPE_NAMES, 68 TAP_TYPE_NAMES, 69 FLING_TYPE_NAMES, 70 TOUCH_TYPE_NAMES, 71 SCROLL_TYPE_NAMES 72 ); 73 74 var RENDERER_FLING_TITLE = 'InputHandlerProxy::HandleGestureFling::started'; 75 76 // TODO(benjhayden) share with rail_ir_finder 77 var CSS_ANIMATION_TITLE = 'Animation'; 78 79 // If there's less than this much time between the end of one event and the 80 // start of the next, then they might be merged. 81 // There was not enough thought given to this value, so if you have any slight 82 // reason to change it, then please do so. It might also be good to split this 83 // into multiple values. 84 var INPUT_MERGE_THRESHOLD_MS = 200; 85 var ANIMATION_MERGE_THRESHOLD_MS = 1; 86 87 // If two MouseWheel events begin this close together, then they're an 88 // Animation, not two responses. 89 var MOUSE_WHEEL_THRESHOLD_MS = 40; 90 91 // If two MouseMoves are more than this far apart, then they're two Responses, 92 // not Animation. 93 var MOUSE_MOVE_THRESHOLD_MS = 40; 94 95 // Strings used to name IRs. 96 var KEYBOARD_IR_NAME = 'Keyboard'; 97 var MOUSE_IR_NAME = 'Mouse'; 98 var MOUSEWHEEL_IR_NAME = 'MouseWheel'; 99 var TAP_IR_NAME = 'Tap'; 100 var PINCH_IR_NAME = 'Pinch'; 101 var FLING_IR_NAME = 'Fling'; 102 var TOUCH_IR_NAME = 'Touch'; 103 var SCROLL_IR_NAME = 'Scroll'; 104 var CSS_IR_NAME = 'CSS'; 105 106 // TODO(benjhayden) Find a better home for this. 107 function compareEvents(x, y) { 108 if (x.start !== y.start) 109 return x.start - y.start; 110 if (x.end !== y.end) 111 return x.end - y.end; 112 if (x.guid && y.guid) 113 return x.guid - y.guid; 114 return 0; 115 } 116 117 function forEventTypesIn(events, typeNames, cb, opt_this) { 118 events.forEach(function(event) { 119 if (typeNames.indexOf(event.typeName) >= 0) { 120 cb.call(opt_this, event); 121 } 122 }); 123 } 124 125 function causedFrame(event) { 126 for (var i = 0; i < event.associatedEvents.length; ++i) { 127 if (event.associatedEvents[i].title === 128 tr.model.helpers.IMPL_RENDERING_STATS) 129 return true; 130 } 131 return false; 132 } 133 134 function getSortedInputEvents(modelHelper) { 135 var inputEvents = []; 136 137 var browserProcess = modelHelper.browserHelper.process; 138 var mainThread = browserProcess.findAtMostOneThreadNamed( 139 'CrBrowserMain'); 140 mainThread.asyncSliceGroup.iterateAllEvents(function(slice) { 141 if (!slice.isTopLevel) 142 return; 143 144 if (!(slice instanceof tr.e.cc.InputLatencyAsyncSlice)) 145 return; 146 147 // TODO(beaudoin): This should never happen but it does. Investigate 148 // the trace linked at in #1567 and remove that when it's fixed. 149 if (isNaN(slice.start) || 150 isNaN(slice.duration) || 151 isNaN(slice.end)) 152 return; 153 154 inputEvents.push(slice); 155 }); 156 157 return inputEvents.sort(compareEvents); 158 } 159 160 function findProtoExpectations(modelHelper, sortedInputEvents) { 161 var protoExpectations = []; 162 // This order is not important. Handlers are independent. 163 var handlers = [ 164 handleKeyboardEvents, 165 handleMouseResponseEvents, 166 handleMouseWheelEvents, 167 handleMouseDragEvents, 168 handleTapResponseEvents, 169 handlePinchEvents, 170 handleFlingEvents, 171 handleTouchEvents, 172 handleScrollEvents, 173 handleCSSAnimations 174 ]; 175 handlers.forEach(function(handler) { 176 protoExpectations.push.apply(protoExpectations, handler( 177 modelHelper, sortedInputEvents)); 178 }); 179 protoExpectations.sort(compareEvents); 180 return protoExpectations; 181 } 182 183 // Every keyboard event is a Response. 184 function handleKeyboardEvents(modelHelper, sortedInputEvents) { 185 var protoExpectations = []; 186 forEventTypesIn(sortedInputEvents, KEYBOARD_TYPE_NAMES, function(event) { 187 var pe = new ProtoExpectation( 188 ProtoExpectation.RESPONSE_TYPE, KEYBOARD_IR_NAME); 189 pe.pushEvent(event); 190 protoExpectations.push(pe); 191 }); 192 return protoExpectations; 193 } 194 195 // Some mouse events can be translated directly into Responses. 196 function handleMouseResponseEvents(modelHelper, sortedInputEvents) { 197 var protoExpectations = []; 198 forEventTypesIn( 199 sortedInputEvents, MOUSE_RESPONSE_TYPE_NAMES, function(event) { 200 var pe = new ProtoExpectation( 201 ProtoExpectation.RESPONSE_TYPE, MOUSE_IR_NAME); 202 pe.pushEvent(event); 203 protoExpectations.push(pe); 204 }); 205 return protoExpectations; 206 } 207 208 // MouseWheel events are caused either by a physical wheel on a physical 209 // mouse, or by a touch-drag gesture on a track-pad. The physical wheel 210 // causes MouseWheel events that are much more spaced out, and have no 211 // chance of hitting 60fps, so they are each turned into separate Response 212 // IRs. The track-pad causes MouseWheel events that are much closer 213 // together, and are expected to be 60fps, so the first event in a sequence 214 // is turned into a Response, and the rest are merged into an Animation. 215 // NB this threshold uses the two events' start times, unlike 216 // ProtoExpectation.isNear, which compares the end time of the previous event 217 // with the start time of the next. 218 function handleMouseWheelEvents(modelHelper, sortedInputEvents) { 219 var protoExpectations = []; 220 var currentPE = undefined; 221 var prevEvent_ = undefined; 222 forEventTypesIn( 223 sortedInputEvents, MOUSE_WHEEL_TYPE_NAMES, function(event) { 224 // Switch prevEvent in one place so that we can early-return later. 225 var prevEvent = prevEvent_; 226 prevEvent_ = event; 227 228 if (currentPE && 229 (prevEvent.start + MOUSE_WHEEL_THRESHOLD_MS) >= event.start) { 230 if (currentPE.irType === ProtoExpectation.ANIMATION_TYPE) { 231 currentPE.pushEvent(event); 232 } else { 233 currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE, 234 MOUSEWHEEL_IR_NAME); 235 currentPE.pushEvent(event); 236 protoExpectations.push(currentPE); 237 } 238 return; 239 } 240 currentPE = new ProtoExpectation( 241 ProtoExpectation.RESPONSE_TYPE, MOUSEWHEEL_IR_NAME); 242 currentPE.pushEvent(event); 243 protoExpectations.push(currentPE); 244 }); 245 return protoExpectations; 246 } 247 248 // Down events followed closely by Up events are click Responses, but the 249 // Response doesn't start until the Up event. 250 // 251 // RRR 252 // DDD UUU 253 // 254 // If there are any Move events in between a Down and an Up, then the Down 255 // and the first Move are a Response, then the rest of the Moves are an 256 // Animation: 257 // 258 // RRRRRRRAAAAAAAAAAAAAAAAAAAA 259 // DDD MMM MMM MMM MMM MMM UUU 260 // 261 function handleMouseDragEvents(modelHelper, sortedInputEvents) { 262 var protoExpectations = []; 263 var currentPE = undefined; 264 var mouseDownEvent = undefined; 265 forEventTypesIn( 266 sortedInputEvents, MOUSE_DRAG_TYPE_NAMES, function(event) { 267 switch (event.typeName) { 268 case INPUT_TYPE.MOUSE_DOWN: 269 if (causedFrame(event)) { 270 var pe = new ProtoExpectation( 271 ProtoExpectation.RESPONSE_TYPE, MOUSE_IR_NAME); 272 pe.pushEvent(event); 273 protoExpectations.push(pe); 274 } else { 275 // Responses typically don't start until the mouse up event. 276 // Add this MouseDown to the Response that starts at the MouseUp. 277 mouseDownEvent = event; 278 } 279 break; 280 // There may be more than 100ms between the start of the mouse down 281 // and the start of the mouse up. Chrome and the web don't start to 282 // respond until the mouse up. ResponseIRs start deducting comfort 283 // at 100ms duration. If more than that 100ms duration is burned 284 // through while waiting for the user to release the 285 // mouse button, then ResponseIR will unfairly start deducting 286 // comfort before Chrome even has a mouse up to respond to. 287 // It is technically possible for a site to afford one response on 288 // mouse down and another on mouse up, but that is an edge case. The 289 // vast majority of mouse downs are not responses. 290 291 case INPUT_TYPE.MOUSE_MOVE: 292 if (!causedFrame(event)) { 293 // Ignore MouseMoves that do not affect the screen. They are not 294 // part of an interaction record by definition. 295 var pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE); 296 pe.pushEvent(event); 297 protoExpectations.push(pe); 298 } else if (!currentPE || 299 !currentPE.isNear(event, MOUSE_MOVE_THRESHOLD_MS)) { 300 // The first MouseMove after a MouseDown or after a while is a 301 // Response. 302 currentPE = new ProtoExpectation( 303 ProtoExpectation.RESPONSE_TYPE, MOUSE_IR_NAME); 304 currentPE.pushEvent(event); 305 if (mouseDownEvent) { 306 currentPE.associatedEvents.push(mouseDownEvent); 307 mouseDownEvent = undefined; 308 } 309 protoExpectations.push(currentPE); 310 } else { 311 // Merge this event into an Animation. 312 if (currentPE.irType === ProtoExpectation.ANIMATION_TYPE) { 313 currentPE.pushEvent(event); 314 } else { 315 currentPE = new ProtoExpectation( 316 ProtoExpectation.ANIMATION_TYPE, MOUSE_IR_NAME); 317 currentPE.pushEvent(event); 318 protoExpectations.push(currentPE); 319 } 320 } 321 break; 322 323 case INPUT_TYPE.MOUSE_UP: 324 if (!mouseDownEvent) { 325 var pe = new ProtoExpectation( 326 causedFrame(event) ? ProtoExpectation.RESPONSE_TYPE : 327 ProtoExpectation.IGNORED_TYPE, 328 MOUSE_IR_NAME); 329 pe.pushEvent(event); 330 protoExpectations.push(pe); 331 break; 332 } 333 334 if (currentPE) { 335 currentPE.pushEvent(event); 336 } else { 337 currentPE = new ProtoExpectation( 338 ProtoExpectation.RESPONSE_TYPE, MOUSE_IR_NAME); 339 if (mouseDownEvent) 340 currentPE.associatedEvents.push(mouseDownEvent); 341 currentPE.pushEvent(event); 342 protoExpectations.push(currentPE); 343 } 344 mouseDownEvent = undefined; 345 currentPE = undefined; 346 break; 347 } 348 }); 349 if (mouseDownEvent) { 350 currentPE = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE); 351 currentPE.pushEvent(mouseDownEvent); 352 protoExpectations.push(currentPE); 353 } 354 return protoExpectations; 355 } 356 357 // Solitary Tap events are simple Responses: 358 // 359 // RRR 360 // TTT 361 // 362 // TapDowns are part of Responses. 363 // 364 // RRRRRRR 365 // DDD TTT 366 // 367 // TapCancels are part of Responses, which seems strange. They always go 368 // with scrolls, so they'll probably be merged with scroll Responses. 369 // TapCancels can take a significant amount of time and account for a 370 // significant amount of work, which should be grouped with the scroll IRs 371 // if possible. 372 // 373 // RRRRRRR 374 // DDD CCC 375 // 376 function handleTapResponseEvents(modelHelper, sortedInputEvents) { 377 var protoExpectations = []; 378 var currentPE = undefined; 379 forEventTypesIn(sortedInputEvents, TAP_TYPE_NAMES, function(event) { 380 switch (event.typeName) { 381 case INPUT_TYPE.TAP_DOWN: 382 currentPE = new ProtoExpectation( 383 ProtoExpectation.RESPONSE_TYPE, TAP_IR_NAME); 384 currentPE.pushEvent(event); 385 protoExpectations.push(currentPE); 386 break; 387 388 case INPUT_TYPE.TAP: 389 if (currentPE) { 390 currentPE.pushEvent(event); 391 } else { 392 // Sometimes we get Tap events with no TapDown, sometimes we get 393 // TapDown events. Handle both. 394 currentPE = new ProtoExpectation( 395 ProtoExpectation.RESPONSE_TYPE, TAP_IR_NAME); 396 currentPE.pushEvent(event); 397 protoExpectations.push(currentPE); 398 } 399 currentPE = undefined; 400 break; 401 402 case INPUT_TYPE.TAP_CANCEL: 403 if (!currentPE) { 404 var pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE); 405 pe.pushEvent(event); 406 protoExpectations.push(pe); 407 break; 408 } 409 410 if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) { 411 currentPE.pushEvent(event); 412 } else { 413 currentPE = new ProtoExpectation( 414 ProtoExpectation.RESPONSE_TYPE, TAP_IR_NAME); 415 currentPE.pushEvent(event); 416 protoExpectations.push(currentPE); 417 } 418 currentPE = undefined; 419 break; 420 } 421 }); 422 return protoExpectations; 423 } 424 425 // The PinchBegin and the first PinchUpdate comprise a Response, then the 426 // rest of the PinchUpdates comprise an Animation. 427 // 428 // RRRRRRRAAAAAAAAAAAAAAAAAAAA 429 // BBB UUU UUU UUU UUU UUU EEE 430 // 431 function handlePinchEvents(modelHelper, sortedInputEvents) { 432 var protoExpectations = []; 433 var currentPE = undefined; 434 var sawFirstUpdate = false; 435 var modelBounds = modelHelper.model.bounds; 436 forEventTypesIn(sortedInputEvents, PINCH_TYPE_NAMES, function(event) { 437 switch (event.typeName) { 438 case INPUT_TYPE.PINCH_BEGIN: 439 if (currentPE && 440 currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) { 441 currentPE.pushEvent(event); 442 break; 443 } 444 currentPE = new ProtoExpectation( 445 ProtoExpectation.RESPONSE_TYPE, PINCH_IR_NAME); 446 currentPE.pushEvent(event); 447 currentPE.isAnimationBegin = true; 448 protoExpectations.push(currentPE); 449 sawFirstUpdate = false; 450 break; 451 452 case INPUT_TYPE.PINCH_UPDATE: 453 // Like ScrollUpdates, the Begin and the first Update constitute a 454 // Response, then the rest of the Updates constitute an Animation 455 // that begins when the Response ends. If the user pauses in the 456 // middle of an extended pinch gesture, then multiple Animations 457 // will be created. 458 if (!currentPE || 459 ((currentPE.irType === ProtoExpectation.RESPONSE_TYPE) && 460 sawFirstUpdate) || 461 !currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) { 462 currentPE = new ProtoExpectation( 463 ProtoExpectation.ANIMATION_TYPE, PINCH_IR_NAME); 464 currentPE.pushEvent(event); 465 protoExpectations.push(currentPE); 466 } else { 467 currentPE.pushEvent(event); 468 sawFirstUpdate = true; 469 } 470 break; 471 472 case INPUT_TYPE.PINCH_END: 473 if (currentPE) { 474 currentPE.pushEvent(event); 475 } else { 476 var pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE); 477 pe.pushEvent(event); 478 protoExpectations.push(pe); 479 } 480 currentPE = undefined; 481 break; 482 } 483 }); 484 return protoExpectations; 485 } 486 487 // Flings are defined by 3 types of events: FlingStart, FlingCancel, and the 488 // renderer fling event. Flings do not begin with a Response. Flings end 489 // either at the beginning of a FlingCancel, or at the end of the renderer 490 // fling event. 491 // 492 // AAAAAAAAAAAAAAAAAAAAAAAAAA 493 // SSS 494 // RRRRRRRRRRRRRRRRRRRRRR 495 // 496 // 497 // AAAAAAAAAAA 498 // SSS CCC 499 // 500 function handleFlingEvents(modelHelper, sortedInputEvents) { 501 var protoExpectations = []; 502 var currentPE = undefined; 503 504 function isRendererFling(event) { 505 return event.title === RENDERER_FLING_TITLE; 506 } 507 var browserHelper = modelHelper.browserHelper; 508 var flingEvents = browserHelper.getAllAsyncSlicesMatching( 509 isRendererFling); 510 511 forEventTypesIn(sortedInputEvents, FLING_TYPE_NAMES, function(event) { 512 flingEvents.push(event); 513 }); 514 flingEvents.sort(compareEvents); 515 516 flingEvents.forEach(function(event) { 517 if (event.title === RENDERER_FLING_TITLE) { 518 if (currentPE) { 519 currentPE.pushEvent(event); 520 } else { 521 currentPE = new ProtoExpectation( 522 ProtoExpectation.ANIMATION_TYPE, FLING_IR_NAME); 523 currentPE.pushEvent(event); 524 protoExpectations.push(currentPE); 525 } 526 return; 527 } 528 529 switch (event.typeName) { 530 case INPUT_TYPE.FLING_START: 531 if (currentPE) { 532 console.error('Another FlingStart? File a bug with this trace!'); 533 currentPE.pushEvent(event); 534 } else { 535 currentPE = new ProtoExpectation( 536 ProtoExpectation.ANIMATION_TYPE, FLING_IR_NAME); 537 currentPE.pushEvent(event); 538 // Set end to an invalid value so that it can be noticed and fixed 539 // later. 540 currentPE.end = 0; 541 protoExpectations.push(currentPE); 542 } 543 break; 544 545 case INPUT_TYPE.FLING_CANCEL: 546 if (currentPE) { 547 currentPE.pushEvent(event); 548 // FlingCancel events start when TouchStart events start, which is 549 // typically when a Response starts. FlingCancel events end when 550 // chrome acknowledges them, not when they update the screen. So 551 // there might be one more frame during the FlingCancel, after 552 // this Animation ends. That won't affect the scoring algorithms, 553 // and it will make the IRs look more correct if they don't 554 // overlap unnecessarily. 555 currentPE.end = event.start; 556 currentPE = undefined; 557 } else { 558 var pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE); 559 pe.pushEvent(event); 560 protoExpectations.push(pe); 561 } 562 break; 563 } 564 }); 565 // If there was neither a FLING_CANCEL nor a renderer fling after the 566 // FLING_START, then assume that it ends at the end of the model, so set 567 // the end of currentPE to the end of the model. 568 if (currentPE && !currentPE.end) 569 currentPE.end = modelHelper.model.bounds.max; 570 return protoExpectations; 571 } 572 573 // The TouchStart and the first TouchMove comprise a Response, then the 574 // rest of the TouchMoves comprise an Animation. 575 // 576 // RRRRRRRAAAAAAAAAAAAAAAAAAAA 577 // SSS MMM MMM MMM MMM MMM EEE 578 // 579 // If there are no TouchMove events in between a TouchStart and a TouchEnd, 580 // then it's just a Response. 581 // 582 // RRRRRRR 583 // SSS EEE 584 // 585 function handleTouchEvents(modelHelper, sortedInputEvents) { 586 var protoExpectations = []; 587 var currentPE = undefined; 588 var sawFirstMove = false; 589 forEventTypesIn(sortedInputEvents, TOUCH_TYPE_NAMES, function(event) { 590 switch (event.typeName) { 591 case INPUT_TYPE.TOUCH_START: 592 if (currentPE) { 593 // NB: currentPE will probably be merged with something from 594 // handlePinchEvents(). Multiple TouchStart events without an 595 // intervening TouchEnd logically implies that multiple fingers 596 // are on the screen, so this is probably a pinch gesture. 597 currentPE.pushEvent(event); 598 } else { 599 currentPE = new ProtoExpectation( 600 ProtoExpectation.RESPONSE_TYPE, TOUCH_IR_NAME); 601 currentPE.pushEvent(event); 602 currentPE.isAnimationBegin = true; 603 protoExpectations.push(currentPE); 604 sawFirstMove = false; 605 } 606 break; 607 608 case INPUT_TYPE.TOUCH_MOVE: 609 if (!currentPE) { 610 currentPE = new ProtoExpectation( 611 ProtoExpectation.ANIMATION_TYPE, TOUCH_IR_NAME); 612 currentPE.pushEvent(event); 613 protoExpectations.push(currentPE); 614 break; 615 } 616 617 // Like Scrolls and Pinches, the Response is defined to be the 618 // TouchStart plus the first TouchMove, then the rest of the 619 // TouchMoves constitute an Animation. 620 if ((sawFirstMove && 621 (currentPE.irType === ProtoExpectation.RESPONSE_TYPE)) || 622 !currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) { 623 // If there's already a touchmove in the currentPE or it's not 624 // near event, then finish it and start a new animation. 625 var prevEnd = currentPE.end; 626 currentPE = new ProtoExpectation( 627 ProtoExpectation.ANIMATION_TYPE, TOUCH_IR_NAME); 628 currentPE.pushEvent(event); 629 // It's possible for there to be a gap between TouchMoves, but 630 // that doesn't mean that there should be an Idle IR there. 631 currentPE.start = prevEnd; 632 protoExpectations.push(currentPE); 633 } else { 634 currentPE.pushEvent(event); 635 sawFirstMove = true; 636 } 637 break; 638 639 case INPUT_TYPE.TOUCH_END: 640 if (!currentPE) { 641 var pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE); 642 pe.pushEvent(event); 643 protoExpectations.push(pe); 644 break; 645 } 646 if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) { 647 currentPE.pushEvent(event); 648 } else { 649 var pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE); 650 pe.pushEvent(event); 651 protoExpectations.push(pe); 652 } 653 currentPE = undefined; 654 break; 655 } 656 }); 657 return protoExpectations; 658 } 659 660 // The first ScrollBegin and the first ScrollUpdate comprise a Response, 661 // then the rest comprise an Animation. 662 // 663 // RRRRRRRAAAAAAAAAAAAAAAAAAAA 664 // BBB UUU UUU UUU UUU UUU EEE 665 // 666 function handleScrollEvents(modelHelper, sortedInputEvents) { 667 var protoExpectations = []; 668 var currentPE = undefined; 669 var sawFirstUpdate = false; 670 forEventTypesIn(sortedInputEvents, SCROLL_TYPE_NAMES, function(event) { 671 switch (event.typeName) { 672 case INPUT_TYPE.SCROLL_BEGIN: 673 // Always begin a new PE even if there already is one, unlike 674 // PinchBegin. 675 currentPE = new ProtoExpectation( 676 ProtoExpectation.RESPONSE_TYPE, SCROLL_IR_NAME); 677 currentPE.pushEvent(event); 678 currentPE.isAnimationBegin = true; 679 protoExpectations.push(currentPE); 680 sawFirstUpdate = false; 681 break; 682 683 case INPUT_TYPE.SCROLL_UPDATE: 684 if (currentPE) { 685 if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS) && 686 ((currentPE.irType === ProtoExpectation.ANIMATION_TYPE) || 687 !sawFirstUpdate)) { 688 currentPE.pushEvent(event); 689 sawFirstUpdate = true; 690 } else { 691 currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE, 692 SCROLL_IR_NAME); 693 currentPE.pushEvent(event); 694 protoExpectations.push(currentPE); 695 } 696 } else { 697 // ScrollUpdate without ScrollBegin. 698 currentPE = new ProtoExpectation( 699 ProtoExpectation.ANIMATION_TYPE, SCROLL_IR_NAME); 700 currentPE.pushEvent(event); 701 protoExpectations.push(currentPE); 702 } 703 break; 704 705 case INPUT_TYPE.SCROLL_END: 706 if (!currentPE) { 707 console.error('ScrollEnd without ScrollUpdate? ' + 708 'File a bug with this trace!'); 709 var pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE); 710 pe.pushEvent(event); 711 protoExpectations.push(pe); 712 break; 713 } 714 currentPE.pushEvent(event); 715 break; 716 } 717 }); 718 return protoExpectations; 719 } 720 721 // CSS Animations are merged into Animations when they intersect. 722 function handleCSSAnimations(modelHelper, sortedInputEvents) { 723 var animationEvents = modelHelper.browserHelper. 724 getAllAsyncSlicesMatching(function(event) { 725 return ((event.title === CSS_ANIMATION_TITLE) && 726 (event.duration > 0)); 727 }); 728 729 var animationRanges = []; 730 animationEvents.forEach(function(event) { 731 var rendererHelper = new tr.model.helpers.ChromeRendererHelper( 732 modelHelper, event.parentContainer.parent); 733 animationRanges.push({ 734 min: event.start, 735 max: event.end, 736 event: event, 737 frames: rendererHelper.getFrameEventsInRange( 738 tr.model.helpers.IMPL_FRAMETIME_TYPE, 739 tr.b.Range.fromExplicitRange(event.start, event.end)) 740 }); 741 }); 742 743 function merge(ranges) { 744 var protoExpectation = new ProtoExpectation( 745 ProtoExpectation.ANIMATION_TYPE, CSS_IR_NAME); 746 ranges.forEach(function(range) { 747 protoExpectation.pushEvent(range.event); 748 protoExpectation.associatedEvents.addEventSet(range.frames); 749 }); 750 return protoExpectation; 751 } 752 753 return tr.b.mergeRanges(animationRanges, 754 ANIMATION_MERGE_THRESHOLD_MS, 755 merge); 756 } 757 758 function postProcessProtoExpectations(protoExpectations) { 759 // protoExpectations is input only. Returns a modified set of 760 // ProtoExpectations. The order is important. 761 protoExpectations = mergeIntersectingResponses(protoExpectations); 762 protoExpectations = mergeIntersectingAnimations(protoExpectations); 763 protoExpectations = fixResponseAnimationStarts(protoExpectations); 764 protoExpectations = fixTapResponseTouchAnimations(protoExpectations); 765 return protoExpectations; 766 } 767 768 // TouchStarts happen at the same time as ScrollBegins. 769 // It's easier to let multiple handlers create multiple overlapping 770 // Responses and then merge them, rather than make the handlers aware of the 771 // other handlers' PEs. 772 // 773 // For example: 774 // RR 775 // RRR -> RRRRR 776 // RR 777 // 778 // protoExpectations is input only. 779 // Returns a modified set of ProtoExpectations. 780 function mergeIntersectingResponses(protoExpectations) { 781 var newPEs = []; 782 while (protoExpectations.length) { 783 var pe = protoExpectations.shift(); 784 newPEs.push(pe); 785 786 // Only consider Responses for now. 787 if (pe.irType !== ProtoExpectation.RESPONSE_TYPE) 788 continue; 789 790 for (var i = 0; i < protoExpectations.length; ++i) { 791 var otherPE = protoExpectations[i]; 792 793 if (otherPE.irType !== pe.irType) 794 continue; 795 796 if (!otherPE.intersects(pe)) 797 continue; 798 799 // Don't merge together Responses of the same type. 800 // If handleTouchEvents wanted two of its Responses to be merged, then 801 // it would have made them that way to begin with. 802 var typeNames = pe.associatedEvents.map(function(event) { 803 return event.typeName; 804 }); 805 if (otherPE.containsTypeNames(typeNames)) 806 continue; 807 808 pe.merge(otherPE); 809 protoExpectations.splice(i, 1); 810 // Don't skip the next otherPE! 811 --i; 812 } 813 } 814 return newPEs; 815 } 816 817 // An animation is simply an expectation of 60fps between start and end. 818 // If two animations overlap, then merge them. 819 // 820 // For example: 821 // AA 822 // AAA -> AAAAA 823 // AA 824 // 825 // protoExpectations is input only. 826 // Returns a modified set of ProtoExpectations. 827 function mergeIntersectingAnimations(protoExpectations) { 828 var newPEs = []; 829 while (protoExpectations.length) { 830 var pe = protoExpectations.shift(); 831 newPEs.push(pe); 832 833 // Only consider Animations for now. 834 if (pe.irType !== ProtoExpectation.ANIMATION_TYPE) 835 continue; 836 837 var isCSS = pe.containsSliceTitle(CSS_ANIMATION_TITLE); 838 var isFling = pe.containsTypeNames([INPUT_TYPE.FLING_START]); 839 840 for (var i = 0; i < protoExpectations.length; ++i) { 841 var otherPE = protoExpectations[i]; 842 843 if (otherPE.irType !== pe.irType) 844 continue; 845 846 // Don't merge CSS Animations with any other types. 847 if (isCSS != otherPE.containsSliceTitle(CSS_ANIMATION_TITLE)) 848 continue; 849 850 if (!otherPE.intersects(pe)) 851 continue; 852 853 // Don't merge Fling Animations with any other types. 854 if (isFling != otherPE.containsTypeNames([INPUT_TYPE.FLING_START])) 855 continue; 856 857 pe.merge(otherPE); 858 protoExpectations.splice(i, 1); 859 // Don't skip the next otherPE! 860 --i; 861 } 862 } 863 return newPEs; 864 } 865 866 // The ends of responses frequently overlap the starts of animations. 867 // Fix the animations to reflect the fact that the user can only start to 868 // expect 60fps after the response. 869 // 870 // For example: 871 // RRR -> RRRAA 872 // AAAA 873 // 874 // protoExpectations is input only. 875 // Returns a modified set of ProtoExpectations. 876 function fixResponseAnimationStarts(protoExpectations) { 877 protoExpectations.forEach(function(ape) { 878 // Only consider animations for now. 879 if (ape.irType !== ProtoExpectation.ANIMATION_TYPE) 880 return; 881 882 protoExpectations.forEach(function(rpe) { 883 // Only consider responses for now. 884 if (rpe.irType !== ProtoExpectation.RESPONSE_TYPE) 885 return; 886 887 // Only consider responses that end during the animation. 888 if (!ape.containsTimestampInclusive(rpe.end)) 889 return; 890 891 // Ignore Responses that are entirely contained by the animation. 892 if (ape.containsTimestampInclusive(rpe.start)) 893 return; 894 895 // Move the animation start to the response end. 896 ape.start = rpe.end; 897 }); 898 }); 899 return protoExpectations; 900 } 901 902 // Merge Tap Responses that overlap Touch-only Animations. 903 // https://github.com/catapult-project/catapult/issues/1431 904 function fixTapResponseTouchAnimations(protoExpectations) { 905 function isTapResponse(pe) { 906 return (pe.irType === ProtoExpectation.RESPONSE_TYPE) && 907 pe.containsTypeNames([INPUT_TYPE.TAP]); 908 } 909 function isTouchAnimation(pe) { 910 return (pe.irType === ProtoExpectation.ANIMATION_TYPE) && 911 pe.containsTypeNames([INPUT_TYPE.TOUCH_MOVE]) && 912 !pe.containsTypeNames([ 913 INPUT_TYPE.SCROLL_UPDATE, INPUT_TYPE.PINCH_UPDATE]); 914 } 915 var newPEs = []; 916 while (protoExpectations.length) { 917 var pe = protoExpectations.shift(); 918 newPEs.push(pe); 919 920 // protoExpectations are sorted by start time, and we don't know whether 921 // the Tap Response or the Touch Animation will be first 922 var peIsTapResponse = isTapResponse(pe); 923 var peIsTouchAnimation = isTouchAnimation(pe); 924 if (!peIsTapResponse && !peIsTouchAnimation) 925 continue; 926 927 for (var i = 0; i < protoExpectations.length; ++i) { 928 var otherPE = protoExpectations[i]; 929 930 if (!otherPE.intersects(pe)) 931 continue; 932 933 if (peIsTapResponse && !isTouchAnimation(otherPE)) 934 continue; 935 936 if (peIsTouchAnimation && !isTapResponse(otherPE)) 937 continue; 938 939 // pe might be the Touch Animation, but the merged ProtoExpectation 940 // should be a Response. 941 pe.irType = ProtoExpectation.RESPONSE_TYPE; 942 943 pe.merge(otherPE); 944 protoExpectations.splice(i, 1); 945 // Don't skip the next otherPE! 946 --i; 947 } 948 } 949 return newPEs; 950 } 951 952 // Check that none of the handlers accidentally ignored an input event. 953 function checkAllInputEventsHandled(sortedInputEvents, protoExpectations) { 954 var handledEvents = []; 955 protoExpectations.forEach(function(protoExpectation) { 956 protoExpectation.associatedEvents.forEach(function(event) { 957 if (handledEvents.indexOf(event) >= 0) { 958 console.error('double-handled event', event.typeName, 959 parseInt(event.start), parseInt(event.end), protoExpectation); 960 return; 961 } 962 handledEvents.push(event); 963 }); 964 }); 965 966 sortedInputEvents.forEach(function(event) { 967 if (handledEvents.indexOf(event) < 0) { 968 console.error('UNHANDLED INPUT EVENT!', 969 event.typeName, parseInt(event.start), parseInt(event.end)); 970 } 971 }); 972 } 973 974 // Find ProtoExpectations, post-process them, convert them to real IRs. 975 function findInputExpectations(modelHelper) { 976 var sortedInputEvents = getSortedInputEvents(modelHelper); 977 var protoExpectations = findProtoExpectations( 978 modelHelper, sortedInputEvents); 979 protoExpectations = postProcessProtoExpectations(protoExpectations); 980 checkAllInputEventsHandled(sortedInputEvents, protoExpectations); 981 982 var irs = []; 983 protoExpectations.forEach(function(protoExpectation) { 984 var ir = protoExpectation.createInteractionRecord(modelHelper.model); 985 if (ir) 986 irs.push(ir); 987 }); 988 return irs; 989 } 990 991 return { 992 findInputExpectations: findInputExpectations, 993 compareEvents: compareEvents, 994 CSS_ANIMATION_TITLE: CSS_ANIMATION_TITLE 995 }; 996}); 997</script> 998