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