summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html')
-rw-r--r--chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html1409
1 files changed, 1409 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html b/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html
new file mode 100644
index 00000000000..464a41853d7
--- /dev/null
+++ b/chromium/third_party/catapult/tracing/tracing/importer/find_input_expectations.html
@@ -0,0 +1,1409 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/tracing/base/math/range_utils.html">
+<link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html">
+<link rel="import" href="/tracing/importer/proto_expectation.html">
+<link rel="import" href="/tracing/model/user_model/user_expectation.html">
+
+<script>
+'use strict';
+
+tr.exportTo('tr.importer', function() {
+ const ProtoExpectation = tr.importer.ProtoExpectation;
+ const INITIATOR_TYPE = tr.model.um.INITIATOR_TYPE;
+ const INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES;
+
+ const KEYBOARD_TYPE_NAMES = [
+ INPUT_TYPE.CHAR,
+ INPUT_TYPE.KEY_DOWN_RAW,
+ INPUT_TYPE.KEY_DOWN,
+ INPUT_TYPE.KEY_UP
+ ];
+ const MOUSE_RESPONSE_TYPE_NAMES = [
+ INPUT_TYPE.CLICK,
+ INPUT_TYPE.CONTEXT_MENU
+ ];
+ const MOUSE_WHEEL_TYPE_NAMES = [
+ INPUT_TYPE.MOUSE_WHEEL
+ ];
+ const MOUSE_DRAG_TYPE_NAMES = [
+ INPUT_TYPE.MOUSE_DOWN,
+ INPUT_TYPE.MOUSE_MOVE,
+ INPUT_TYPE.MOUSE_UP
+ ];
+ const TAP_TYPE_NAMES = [
+ INPUT_TYPE.TAP,
+ INPUT_TYPE.TAP_CANCEL,
+ INPUT_TYPE.TAP_DOWN
+ ];
+ const PINCH_TYPE_NAMES = [
+ INPUT_TYPE.PINCH_BEGIN,
+ INPUT_TYPE.PINCH_END,
+ INPUT_TYPE.PINCH_UPDATE
+ ];
+ const FLING_TYPE_NAMES = [
+ INPUT_TYPE.FLING_CANCEL,
+ INPUT_TYPE.FLING_START
+ ];
+ const TOUCH_TYPE_NAMES = [
+ INPUT_TYPE.TOUCH_END,
+ INPUT_TYPE.TOUCH_MOVE,
+ INPUT_TYPE.TOUCH_START
+ ];
+ const SCROLL_TYPE_NAMES = [
+ INPUT_TYPE.SCROLL_BEGIN,
+ INPUT_TYPE.SCROLL_END,
+ INPUT_TYPE.SCROLL_UPDATE
+ ];
+ const ALL_HANDLED_TYPE_NAMES = [].concat(
+ KEYBOARD_TYPE_NAMES,
+ MOUSE_RESPONSE_TYPE_NAMES,
+ MOUSE_WHEEL_TYPE_NAMES,
+ MOUSE_DRAG_TYPE_NAMES,
+ PINCH_TYPE_NAMES,
+ TAP_TYPE_NAMES,
+ FLING_TYPE_NAMES,
+ TOUCH_TYPE_NAMES,
+ SCROLL_TYPE_NAMES
+ );
+
+ const RENDERER_FLING_TITLE = 'InputHandlerProxy::HandleGestureFling::started';
+ const PLAYBACK_EVENT_TITLE = 'VideoPlayback';
+
+ const CSS_ANIMATION_TITLE = 'Animation';
+
+ const VR_COUNTER_NAMES = [
+ 'gpu.WebVR FPS',
+ 'gpu.WebVR frame time (ms)',
+ 'gpu.WebVR pose prediction (ms)',
+ ];
+ const VR_EVENT_NAMES = [
+ 'VrShellGl::AcquireFrame',
+ 'VrShellGl::DrawFrame',
+ 'VrShellGl::DrawSubmitFrameWhenReady',
+ 'VrShellGl::DrawUiView',
+ 'VrShellGl::UpdateController',
+ ];
+ /* 1s is a bit arbitrary, but it reliably avoids all the jank caused by
+ * VR entry.
+ */
+ const VR_RESPONSE_MS = 1000;
+
+ /**
+ * If there's less than this much time between the end of one event and the
+ * start of the next, then they might be merged.
+ * There was not enough thought given to this value, so if you have any slight
+ * reason to change it, then please do so. It might also be good to split this
+ * into multiple values.
+ */
+ const INPUT_MERGE_THRESHOLD_MS = 200;
+ const ANIMATION_MERGE_THRESHOLD_MS = 32; // 2x 60FPS frames
+
+ /**
+ * If two MouseWheel events begin this close together, then they're an
+ * Animation, not two responses.
+ */
+ const MOUSE_WHEEL_THRESHOLD_MS = 40;
+
+ /**
+ * If two MouseMoves are more than this far apart, then they're two Responses,
+ * not Animation.
+ */
+ const MOUSE_MOVE_THRESHOLD_MS = 40;
+
+ // TODO(#3813) Move this.
+ function compareEvents(x, y) {
+ if (x.start !== y.start) {
+ return x.start - y.start;
+ }
+ if (x.end !== y.end) {
+ return x.end - y.end;
+ }
+ if (x.guid && y.guid) {
+ return x.guid - y.guid;
+ }
+ return 0;
+ }
+
+ function forEventTypesIn(events, typeNames, cb, opt_this) {
+ events.forEach(function(event) {
+ if (typeNames.indexOf(event.typeName) >= 0) {
+ cb.call(opt_this, event);
+ }
+ });
+ }
+
+ function causedFrame(event) {
+ return event.associatedEvents.some(isImplFrameEvent);
+ }
+
+ function getSortedFrameEventsByProcess(modelHelper) {
+ const frameEventsByPid = {};
+ for (const [pid, rendererHelper] of
+ Object.entries(modelHelper.rendererHelpers)) {
+ frameEventsByPid[pid] = rendererHelper.getFrameEventsInRange(
+ tr.model.helpers.IMPL_FRAMETIME_TYPE, modelHelper.model.bounds);
+ }
+ return frameEventsByPid;
+ }
+
+ function getSortedInputEvents(modelHelper) {
+ const inputEvents = [];
+
+ const browserProcess = modelHelper.browserHelper.process;
+ const mainThread = browserProcess.findAtMostOneThreadNamed(
+ 'CrBrowserMain');
+ for (const slice of mainThread.asyncSliceGroup.getDescendantEvents()) {
+ if (!slice.isTopLevel) continue;
+
+ if (!(slice instanceof tr.e.cc.InputLatencyAsyncSlice)) continue;
+
+ if (isNaN(slice.start) ||
+ isNaN(slice.duration) ||
+ isNaN(slice.end)) {
+ continue;
+ }
+
+ inputEvents.push(slice);
+ }
+
+ return inputEvents.sort(compareEvents);
+ }
+
+ function findProtoExpectations(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ // This order is not important. Handlers are independent.
+ const handlers = [
+ handleKeyboardEvents,
+ handleMouseResponseEvents,
+ handleMouseWheelEvents,
+ handleMouseDragEvents,
+ handleTapResponseEvents,
+ handlePinchEvents,
+ handleFlingEvents,
+ handleTouchEvents,
+ handleScrollEvents,
+ handleCSSAnimations,
+ handleWebGLAnimations,
+ handleVideoAnimations,
+ handleVrAnimations,
+ ];
+ handlers.forEach(function(handler) {
+ protoExpectations.push.apply(protoExpectations, handler(
+ modelHelper, sortedInputEvents, warn));
+ });
+ protoExpectations.sort(compareEvents);
+ return protoExpectations;
+ }
+
+ /**
+ * Every keyboard event is a Response.
+ */
+ function handleKeyboardEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ forEventTypesIn(sortedInputEvents, KEYBOARD_TYPE_NAMES, function(event) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.KEYBOARD);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Some mouse events can be translated directly into Responses.
+ */
+ function handleMouseResponseEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_RESPONSE_TYPE_NAMES, function(event) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ });
+ return protoExpectations;
+ }
+ /**
+ * MouseWheel events are caused either by a physical wheel on a physical
+ * mouse, or by a touch-drag gesture on a track-pad. The physical wheel
+ * causes MouseWheel events that are much more spaced out, and have no
+ * chance of hitting 60fps, so they are each turned into separate Response
+ * UEs. The track-pad causes MouseWheel events that are much closer
+ * together, and are expected to be 60fps, so the first event in a sequence
+ * is turned into a Response, and the rest are merged into an Animation.
+ * NB this threshold uses the two events' start times, unlike
+ * ProtoExpectation.isNear, which compares the end time of the previous event
+ * with the start time of the next.
+ */
+ function handleMouseWheelEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let prevEvent_ = undefined;
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_WHEEL_TYPE_NAMES, function(event) {
+ // Switch prevEvent in one place so that we can early-return later.
+ const prevEvent = prevEvent_;
+ prevEvent_ = event;
+
+ if (currentPE &&
+ (prevEvent.start + MOUSE_WHEEL_THRESHOLD_MS) >= event.start) {
+ if (currentPE.type === ProtoExpectation.ANIMATION_TYPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.MOUSE_WHEEL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ return;
+ }
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE_WHEEL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Down events followed closely by Up events are click Responses, but the
+ * Response doesn't start until the Up event.
+ *
+ * RRR
+ * DDD UUU
+ *
+ * If there are any Move events in between a Down and an Up, then the Down
+ * and the first Move are a Response, then the rest of the Moves are an
+ * Animation:
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * DDD MMM MMM MMM MMM MMM UUU
+ */
+ function handleMouseDragEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let mouseDownEvent = undefined;
+ forEventTypesIn(
+ sortedInputEvents, MOUSE_DRAG_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.MOUSE_DOWN:
+ if (causedFrame(event)) {
+ const pe = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ } else {
+ // Responses typically don't start until the mouse up event.
+ // Add this MouseDown to the Response that starts at the
+ // MouseUp.
+ mouseDownEvent = event;
+ }
+ break;
+
+ // There may be more than 100ms between the start of the mouse
+ // down and the start of the mouse up. Chrome and the web don't
+ // start to respond until the mouse up. Responses start deducting
+ // comfort at 100ms duration. If more than that 100ms duration is
+ // burned through while waiting for the user to release the mouse
+ // button, then ResponseExpectation will unfairly start deducting
+ // comfort before Chrome even has a mouse up to respond to. It is
+ // technically possible for a site to afford one response on mouse
+ // down and another on mouse up, but that is an edge case. The
+ // vast majority of mouse downs are not responses.
+
+ case INPUT_TYPE.MOUSE_MOVE:
+ if (!causedFrame(event)) {
+ // Ignore MouseMoves that do not affect the screen. They are not
+ // part of an interaction record by definition.
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ } else if (!currentPE ||
+ !currentPE.isNear(event, MOUSE_MOVE_THRESHOLD_MS)) {
+ // The first MouseMove after a MouseDown or after a while is a
+ // Response.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ currentPE.pushEvent(event);
+ if (mouseDownEvent) {
+ currentPE.associatedEvents.push(mouseDownEvent);
+ mouseDownEvent = undefined;
+ }
+ protoExpectations.push(currentPE);
+ } else {
+ // Merge this event into an Animation.
+ if (currentPE.type === ProtoExpectation.ANIMATION_TYPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.MOUSE);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ }
+ break;
+
+ case INPUT_TYPE.MOUSE_UP:
+ if (!mouseDownEvent) {
+ const pe = new ProtoExpectation(
+ causedFrame(event) ? ProtoExpectation.RESPONSE_TYPE :
+ ProtoExpectation.IGNORED_TYPE,
+ INITIATOR_TYPE.MOUSE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.MOUSE);
+ if (mouseDownEvent) {
+ currentPE.associatedEvents.push(mouseDownEvent);
+ }
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ mouseDownEvent = undefined;
+ currentPE = undefined;
+ break;
+ }
+ });
+ if (mouseDownEvent) {
+ currentPE = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ currentPE.pushEvent(mouseDownEvent);
+ protoExpectations.push(currentPE);
+ }
+ return protoExpectations;
+ }
+
+ /**
+ * Solitary Tap events are simple Responses:
+ *
+ * RRR
+ * TTT
+ *
+ * TapDowns are part of Responses.
+ *
+ * RRRRRRR
+ * DDD TTT
+ *
+ * TapCancels are part of Responses, which seems strange. They always go
+ * with scrolls, so they'll probably be merged with scroll Responses.
+ * TapCancels can take a significant amount of time and account for a
+ * significant amount of work, which should be grouped with the scroll UEs
+ * if possible.
+ *
+ * RRRRRRR
+ * DDD CCC
+ **/
+ function handleTapResponseEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ forEventTypesIn(sortedInputEvents, TAP_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.TAP_DOWN:
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ break;
+
+ case INPUT_TYPE.TAP:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ // Sometimes we get Tap events with no TapDown, sometimes we get
+ // TapDown events. Handle both.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ currentPE = undefined;
+ break;
+
+ case INPUT_TYPE.TAP_CANCEL:
+ if (!currentPE) {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TAP);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * The PinchBegin and the first PinchUpdate comprise a Response, then the
+ * rest of the PinchUpdates comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * BBB UUU UUU UUU UUU UUU EEE
+ */
+ function handlePinchEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstUpdate = false;
+ const modelBounds = modelHelper.model.bounds;
+ forEventTypesIn(sortedInputEvents, PINCH_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.PINCH_BEGIN:
+ if (currentPE &&
+ currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ break;
+ }
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.PINCH);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstUpdate = false;
+ break;
+
+ case INPUT_TYPE.PINCH_UPDATE:
+ // Like ScrollUpdates, the Begin and the first Update constitute a
+ // Response, then the rest of the Updates constitute an Animation
+ // that begins when the Response ends. If the user pauses in the
+ // middle of an extended pinch gesture, then multiple Animations
+ // will be created.
+ if (!currentPE ||
+ ((currentPE.type === ProtoExpectation.RESPONSE_TYPE) &&
+ sawFirstUpdate) ||
+ !currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.PINCH);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ sawFirstUpdate = true;
+ }
+ break;
+
+ case INPUT_TYPE.PINCH_END:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Flings are defined by 3 types of events: FlingStart, FlingCancel, and the
+ * renderer fling event. Flings do not begin with a Response. Flings end
+ * either at the beginning of a FlingCancel, or at the end of the renderer
+ * fling event.
+ *
+ * AAAAAAAAAAAAAAAAAAAAAAAAAA
+ * SSS
+ * RRRRRRRRRRRRRRRRRRRRRR
+ *
+ *
+ * AAAAAAAAAAA
+ * SSS CCC
+ */
+ function handleFlingEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+
+ function isRendererFling(event) {
+ return event.title === RENDERER_FLING_TITLE;
+ }
+ const browserHelper = modelHelper.browserHelper;
+ const flingEvents = browserHelper.getAllAsyncSlicesMatching(
+ isRendererFling);
+
+ forEventTypesIn(sortedInputEvents, FLING_TYPE_NAMES, function(event) {
+ flingEvents.push(event);
+ });
+ flingEvents.sort(compareEvents);
+
+ flingEvents.forEach(function(event) {
+ if (event.title === RENDERER_FLING_TITLE) {
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.FLING);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ return;
+ }
+
+ switch (event.typeName) {
+ case INPUT_TYPE.FLING_START:
+ if (currentPE) {
+ warn({
+ type: 'UserModelBuilder',
+ message: 'Unexpected FlingStart',
+ showToUser: false,
+ });
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.FLING);
+ currentPE.pushEvent(event);
+ // Set end to an invalid value so that it can be noticed and fixed
+ // later.
+ currentPE.end = 0;
+ protoExpectations.push(currentPE);
+ }
+ break;
+
+ case INPUT_TYPE.FLING_CANCEL:
+ if (currentPE) {
+ currentPE.pushEvent(event);
+ // FlingCancel events start when TouchStart events start, which is
+ // typically when a Response starts. FlingCancel events end when
+ // chrome acknowledges them, not when they update the screen. So
+ // there might be one more frame during the FlingCancel, after
+ // this Animation ends. That won't affect the scoring algorithms,
+ // and it will make the UEs look more correct if they don't
+ // overlap unnecessarily.
+ currentPE.end = event.start;
+ currentPE = undefined;
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ break;
+ }
+ });
+ // If there was neither a FLING_CANCEL nor a renderer fling after the
+ // FLING_START, then assume that it ends at the end of the model, so set
+ // the end of currentPE to the end of the model.
+ if (currentPE && !currentPE.end) {
+ currentPE.end = modelHelper.model.bounds.max;
+ }
+ return protoExpectations;
+ }
+
+ /**
+ * The TouchStart and the first TouchMove comprise a Response, then the
+ * rest of the TouchMoves comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * SSS MMM MMM MMM MMM MMM EEE
+ *
+ * If there are no TouchMove events in between a TouchStart and a TouchEnd,
+ * then it's just a Response.
+ *
+ * RRRRRRR
+ * SSS EEE
+ */
+ function handleTouchEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstMove = false;
+ forEventTypesIn(sortedInputEvents, TOUCH_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.TOUCH_START:
+ if (currentPE) {
+ // NB: currentPE will probably be merged with something from
+ // handlePinchEvents(). Multiple TouchStart events without an
+ // intervening TouchEnd logically implies that multiple fingers
+ // are on the screen, so this is probably a pinch gesture.
+ currentPE.pushEvent(event);
+ } else {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstMove = false;
+ }
+ break;
+
+ case INPUT_TYPE.TOUCH_MOVE:
+ if (!currentPE) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ break;
+ }
+
+ // Like Scrolls and Pinches, the Response is defined to be the
+ // TouchStart plus the first TouchMove, then the rest of the
+ // TouchMoves constitute an Animation.
+ if ((sawFirstMove &&
+ (currentPE.type === ProtoExpectation.RESPONSE_TYPE)) ||
+ !currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ // If there's already a touchmove in the currentPE or it's not
+ // near event, then finish it and start a new animation.
+ const prevEnd = currentPE.end;
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.TOUCH);
+ currentPE.pushEvent(event);
+ // It's possible for there to be a gap between TouchMoves, but
+ // that doesn't mean that there should be an Idle UE there.
+ currentPE.start = prevEnd;
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ sawFirstMove = true;
+ }
+ break;
+
+ case INPUT_TYPE.TOUCH_END:
+ if (!currentPE) {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
+ currentPE.pushEvent(event);
+ } else {
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ }
+ currentPE = undefined;
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * The first ScrollBegin and the first ScrollUpdate comprise a Response,
+ * then the rest comprise an Animation.
+ *
+ * RRRRRRRAAAAAAAAAAAAAAAAAAAA
+ * BBB UUU UUU UUU UUU UUU EEE
+ */
+ function handleScrollEvents(modelHelper, sortedInputEvents, warn) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ let sawFirstUpdate = false;
+ forEventTypesIn(sortedInputEvents, SCROLL_TYPE_NAMES, function(event) {
+ switch (event.typeName) {
+ case INPUT_TYPE.SCROLL_BEGIN:
+ // Always begin a new PE even if there already is one, unlike
+ // PinchBegin.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.RESPONSE_TYPE, INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ currentPE.isAnimationBegin = true;
+ protoExpectations.push(currentPE);
+ sawFirstUpdate = false;
+ break;
+
+ case INPUT_TYPE.SCROLL_UPDATE:
+ if (currentPE) {
+ if (currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS) &&
+ ((currentPE.type === ProtoExpectation.ANIMATION_TYPE) ||
+ !sawFirstUpdate)) {
+ currentPE.pushEvent(event);
+ sawFirstUpdate = true;
+ } else {
+ currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ } else {
+ // ScrollUpdate without ScrollBegin.
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.SCROLL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+ break;
+
+ case INPUT_TYPE.SCROLL_END:
+ if (!currentPE) {
+ warn({
+ type: 'UserModelBuilder',
+ message: 'Unexpected ScrollEnd',
+ showToUser: false,
+ });
+ const pe = new ProtoExpectation(ProtoExpectation.IGNORED_TYPE);
+ pe.pushEvent(event);
+ protoExpectations.push(pe);
+ break;
+ }
+ currentPE.pushEvent(event);
+ break;
+ }
+ });
+ return protoExpectations;
+ }
+
+ /**
+ * Returns proto expectations for video animation events.
+ *
+ * Video animations represent video playback, and are based on
+ * VideoPlayback async events (going from the VideoFrameCompositor::Start
+ * to VideoFrameCompositor::Stop calls)
+ */
+ function handleVideoAnimations(modelHelper, sortedInputEvents, warn) {
+ const events = [];
+ for (const pid in modelHelper.rendererHelpers) {
+ for (const tid in modelHelper.rendererHelpers[pid].process.threads) {
+ for (const asyncSlice of
+ modelHelper.rendererHelpers[pid].process.threads[tid]
+ .asyncSliceGroup.slices) {
+ if (asyncSlice.title === PLAYBACK_EVENT_TITLE) {
+ events.push(asyncSlice);
+ }
+ }
+ }
+ }
+
+ events.sort(tr.importer.compareEvents);
+
+ const protoExpectations = [];
+ for (const event of events) {
+ const currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.VIDEO);
+ currentPE.start = event.start;
+ currentPE.end = event.end;
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ }
+
+ return protoExpectations;
+ }
+
+ /**
+ * Returns proto expectations for VR animation events.
+ */
+ function handleVrAnimations(modelHelper, sortedInputEvents, warn) {
+ const events = [];
+
+ // Find all the processes we should check
+ const processes = [];
+ if (typeof modelHelper.gpuHelper !== 'undefined') {
+ processes.push(modelHelper.gpuHelper.process);
+ }
+ for (const helper of Object.values(modelHelper.rendererHelpers)) {
+ processes.push(helper.process);
+ }
+ for (const helper of Object.values(modelHelper.browserHelpers)) {
+ processes.push(helper.process);
+ }
+
+ // Add all counter samples to the list of events we care about
+ let vrCounterStart = Number.MAX_SAFE_INTEGER;
+ let vrEventStart = Number.MAX_SAFE_INTEGER;
+ for (const proc of processes) {
+ for (const [counterName, counterSeries] of
+ Object.entries(proc.counters)) {
+ if (VR_COUNTER_NAMES.includes(counterName)) {
+ for (const series of counterSeries.series) {
+ for (const sample of series.samples) {
+ events.push(sample);
+ vrCounterStart = Math.min(vrCounterStart, sample.timestamp);
+ }
+ }
+ }
+ }
+ for (const thread of Object.values(proc.threads)) {
+ for (const container of thread.childEventContainers()) {
+ for (const slice of container.slices) {
+ if (VR_EVENT_NAMES.includes(slice.title)) {
+ events.push(slice);
+ vrEventStart = Math.min(vrEventStart, slice.start);
+ }
+ }
+ }
+ }
+ }
+
+ if (events.length === 0) {
+ return [];
+ }
+
+ events.sort(function(x, y) {
+ if (x.range.min !== y.range.min) {
+ return x.range.min - y.range.min;
+ }
+ return x.guid - y.guid;
+ });
+
+ vrCounterStart = (vrCounterStart === Number.MAX_SAFE_INTEGER) ?
+ 0 : vrCounterStart;
+ vrEventStart = (vrEventStart === Number.MAX_SAFE_INTEGER) ?
+ 0 : vrEventStart;
+ const vrAnimationStart = Math.max(vrCounterStart, vrEventStart) +
+ VR_RESPONSE_MS;
+ const responsePE = new ProtoExpectation(ProtoExpectation.RESPONSE_TYPE,
+ INITIATOR_TYPE.VR);
+ const animationPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
+ INITIATOR_TYPE.VR);
+ let lastResponseEvent;
+
+ for (const event of events) {
+ // Categorize the first 1s of VR time as entry/response
+ // TODO(bsheedy): Make this smarter by basing response duration off trace
+ // data instead of a fixed duration
+ if (event.range.min < vrAnimationStart) {
+ if (event instanceof tr.model.CounterSample) {
+ responsePE.pushSample(event);
+ } else {
+ responsePE.pushEvent(event);
+ }
+ lastResponseEvent = event;
+ } else {
+ if (event instanceof tr.model.CounterSample) {
+ animationPE.pushSample(event);
+ } else {
+ animationPE.pushEvent(event);
+ }
+ }
+ }
+
+ // Make sure that there isn't a gap between the two expectations
+ if (lastResponseEvent instanceof tr.model.CounterSample) {
+ animationPE.pushSample(lastResponseEvent);
+ } else {
+ animationPE.pushEvent(lastResponseEvent);
+ }
+ return [responsePE, animationPE];
+ }
+
+ /**
+ * CSS Animations are merged into AnimationExpectations when they intersect.
+ */
+ function handleCSSAnimations(modelHelper, sortedInputEvents, warn) {
+ // First find all the top-level CSS Animation async events.
+ const animationEvents = modelHelper.browserHelper.
+ getAllAsyncSlicesMatching(function(event) {
+ return ((event.title === CSS_ANIMATION_TITLE) &&
+ event.isTopLevel &&
+ (event.duration > 0));
+ });
+
+
+ // Time ranges where animations are actually running will be collected here.
+ // Each element will contain {min, max, animation}.
+ const animationRanges = [];
+
+ // This helper function will be called when a time range is found
+ // during which the animation is actually running.
+ function pushAnimationRange(start, end, animation) {
+ const range = tr.b.math.Range.fromExplicitRange(start, end);
+ range.animation = animation;
+ animationRanges.push(range);
+ }
+
+ animationEvents.forEach(function(animation) {
+ if (animation.subSlices.length === 0) {
+ pushAnimationRange(animation.start, animation.end, animation);
+ } else {
+ // Now run a state machine over the animation's subSlices, which
+ // indicate the animations running/paused/finished states, in order to
+ // find ranges where the animation was actually running.
+ let start = undefined;
+ animation.subSlices.forEach(function(sub) {
+ if ((sub.args.data.state === 'running') &&
+ (start === undefined)) {
+ // It's possible for the state to alternate between running and
+ // pending, but the animation is still running in that case,
+ // so only set start if the state is changing from one of the halted
+ // states.
+ start = sub.start;
+ } else if ((sub.args.data.state === 'paused') ||
+ (sub.args.data.state === 'idle') ||
+ (sub.args.data.state === 'finished')) {
+ if (start === undefined) {
+ // An animation was already running when the trace started.
+ // (Actually, it's possible that the animation was in the 'idle'
+ // state when tracing started, but that should be rare, and will
+ // be fixed when async events are buffered.)
+ // http: //crbug.com/565627
+ start = modelHelper.model.bounds.min;
+ }
+
+ pushAnimationRange(start, sub.start, animation);
+ start = undefined;
+ }
+ });
+
+ // An animation was still running when the
+ // top-level animation event ended.
+ if (start !== undefined) {
+ pushAnimationRange(start, animation.end, animation);
+ }
+ }
+ });
+
+ // Now we have a set of time ranges when css animations were actually
+ // running.
+ // Leave merging intersecting animations to mergeIntersectingAnimations(),
+ // after findFrameEventsForAnimations removes frame-less animations.
+
+ return animationRanges.map(function(range) {
+ const protoExpectation = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.CSS);
+ protoExpectation.start = range.min;
+ protoExpectation.end = range.max;
+ protoExpectation.associatedEvents.push(range.animation);
+ return protoExpectation;
+ });
+ }
+
+ /**
+ * Get all the events (prepareMailbox and serviceScriptedAnimations)
+ * relevant to WebGL. Note that modelHelper is the helper object containing
+ * the model, and mailboxEvents and animationEvents are arrays where the
+ * events are being pushed into (DrawingBuffer::prepareMailbox events go
+ * into mailboxEvents; PageAnimator::serviceScriptedAnimations events go
+ * into animationEvents). The function does not return anything but
+ * modifies mailboxEvents and animationEvents.
+ */
+ function findWebGLEvents(modelHelper, mailboxEvents, animationEvents) {
+ for (const event of modelHelper.model.getDescendantEvents()) {
+ if (event.title === 'DrawingBuffer::prepareMailbox') {
+ mailboxEvents.push(event);
+ } else if (event.title === 'PageAnimator::serviceScriptedAnimations') {
+ animationEvents.push(event);
+ }
+ }
+ }
+
+ /**
+ * Returns a list of events in mailboxEvents that have an event in
+ * animationEvents close by (within ANIMATION_MERGE_THRESHOLD_MS).
+ */
+ function findMailboxEventsNearAnimationEvents(
+ mailboxEvents, animationEvents) {
+ if (animationEvents.length === 0) return [];
+
+ mailboxEvents.sort(compareEvents);
+ animationEvents.sort(compareEvents);
+ const animationIterator = animationEvents[Symbol.iterator]();
+ let animationEvent = animationIterator.next().value;
+
+ const filteredEvents = [];
+
+ // We iterate through the mailboxEvents. With each event, we check if
+ // there is a animationEvent near it, and if so, add it to the result.
+ for (const event of mailboxEvents) {
+ // If the current animationEvent is too far before the mailboxEvent,
+ // we advance until we get to the next animationEvent that is not too
+ // far before the animationEvent.
+ while (animationEvent &&
+ (animationEvent.start < (
+ event.start - ANIMATION_MERGE_THRESHOLD_MS))) {
+ animationEvent = animationIterator.next().value;
+ }
+
+ // If there aren't any more animationEvents, then that means all the
+ // remaining mailboxEvents are too far after the animationEvents, so
+ // we can quit now.
+ if (!animationEvent) break;
+
+ // If there's a animationEvent close to the mailboxEvent, then we push
+ // the current mailboxEvent onto the stack.
+ if (animationEvent.start < (event.start + ANIMATION_MERGE_THRESHOLD_MS)) {
+ filteredEvents.push(event);
+ }
+ }
+ return filteredEvents;
+ }
+
+ /**
+ * Merge consecutive mailbox events into a ProtoExpectation. Note: Only
+ * the drawingBuffer::prepareMailbox events will end up in the
+ * associatedEvents. The PageAnimator::serviceScriptedAnimations events
+ * will not end up in the associatedEvents.
+ */
+ function createProtoExpectationsFromMailboxEvents(mailboxEvents) {
+ const protoExpectations = [];
+ let currentPE = undefined;
+ for (const event of mailboxEvents) {
+ if (currentPE === undefined || !currentPE.isNear(
+ event, ANIMATION_MERGE_THRESHOLD_MS)) {
+ currentPE = new ProtoExpectation(
+ ProtoExpectation.ANIMATION_TYPE, INITIATOR_TYPE.WEBGL);
+ currentPE.pushEvent(event);
+ protoExpectations.push(currentPE);
+ } else {
+ currentPE.pushEvent(event);
+ }
+ }
+ return protoExpectations;
+ }
+
+ // WebGL animations are identified by the DrawingBuffer::prepareMailbox
+ // and PageAnimator::serviceScriptedAnimations events (one of each per frame)
+ // and consecutive frames are merged into the same animation.
+ function handleWebGLAnimations(modelHelper, sortedInputEvents, warn) {
+ // Get the prepareMailbox and scriptedAnimation events.
+ const prepareMailboxEvents = [];
+ const scriptedAnimationEvents = [];
+
+ findWebGLEvents(modelHelper, prepareMailboxEvents, scriptedAnimationEvents);
+ const webGLMailboxEvents = findMailboxEventsNearAnimationEvents(
+ prepareMailboxEvents, scriptedAnimationEvents);
+
+ return createProtoExpectationsFromMailboxEvents(webGLMailboxEvents);
+ }
+
+
+ function postProcessProtoExpectations(modelHelper, protoExpectations) {
+ // protoExpectations is input only. Returns a modified set of
+ // ProtoExpectations. The order is important.
+ protoExpectations = findFrameEventsForAnimations(
+ modelHelper, protoExpectations);
+ protoExpectations = mergeIntersectingResponses(protoExpectations);
+ protoExpectations = mergeIntersectingAnimations(protoExpectations);
+ protoExpectations = fixResponseAnimationStarts(protoExpectations);
+ protoExpectations = fixTapResponseTouchAnimations(protoExpectations);
+ return protoExpectations;
+ }
+
+ /**
+ * TouchStarts happen at the same time as ScrollBegins.
+ * It's easier to let multiple handlers create multiple overlapping
+ * Responses and then merge them, rather than make the handlers aware of the
+ * other handlers' PEs.
+ *
+ * For example:
+ * RR
+ * RRR -> RRRRR
+ * RR
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function mergeIntersectingResponses(protoExpectations) {
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // Only consider Responses for now.
+ if (pe.type !== ProtoExpectation.RESPONSE_TYPE) continue;
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (otherPE.type !== pe.type) continue;
+
+ if (!otherPE.intersects(pe)) continue;
+
+ // Don't merge together Responses of the same type.
+ // If handleTouchEvents wanted two of its Responses to be merged, then
+ // it would have made them that way to begin with.
+ const typeNames = pe.associatedEvents.map(function(event) {
+ return event.typeName;
+ });
+ if (otherPE.containsTypeNames(typeNames)) continue;
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ /**
+ * An animation is simply an expectation of 60fps between start and end.
+ * If two animations overlap, then merge them.
+ *
+ * For example:
+ * AA
+ * AAA -> AAAAA
+ * AA
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function mergeIntersectingAnimations(protoExpectations) {
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // Only consider Animations for now.
+ if (pe.type !== ProtoExpectation.ANIMATION_TYPE) continue;
+
+ const isCSS = pe.initiatorType === INITIATOR_TYPE.CSS;
+ const isFling = pe.containsTypeNames([INPUT_TYPE.FLING_START]);
+ const isVideo = pe.initiatorType === INITIATOR_TYPE.VIDEO;
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (otherPE.type !== pe.type) continue;
+
+ // Don't merge some animation types with others.
+ if ((isCSS && otherPE.initiatorType !== INITIATOR_TYPE.CSS) ||
+ isFling !== otherPE.containsTypeNames([INPUT_TYPE.FLING_START]) ||
+ isVideo && otherPE.initiatorType !== INITIATOR_TYPE.VIDEO ||
+ otherPE.initiatorType === INITIATOR_TYPE.VR) {
+ continue;
+ }
+
+ if (isCSS) {
+ if (!pe.isNear(otherPE, ANIMATION_MERGE_THRESHOLD_MS)) {
+ continue;
+ }
+ } else if (!otherPE.intersects(pe)) {
+ continue;
+ }
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ /**
+ * The ends of responses frequently overlap the starts of animations.
+ * Fix the animations to reflect the fact that the user can only start to
+ * expect 60fps after the response.
+ *
+ * For example:
+ * RRR -> RRRAA
+ * AAAA
+ *
+ * protoExpectations is input only.
+ * Returns a modified set of ProtoExpectations.
+ */
+ function fixResponseAnimationStarts(protoExpectations) {
+ protoExpectations.forEach(function(ape) {
+ // Only consider animations for now.
+ if (ape.type !== ProtoExpectation.ANIMATION_TYPE) {
+ return;
+ }
+
+ protoExpectations.forEach(function(rpe) {
+ // Only consider responses for now.
+ if (rpe.type !== ProtoExpectation.RESPONSE_TYPE) {
+ return;
+ }
+
+ // Only consider responses that end during the animation.
+ if (!ape.containsTimestampInclusive(rpe.end)) {
+ return;
+ }
+
+ // Ignore Responses that are entirely contained by the animation.
+ if (ape.containsTimestampInclusive(rpe.start)) {
+ return;
+ }
+
+ // Move the animation start to the response end.
+ ape.start = rpe.end;
+ // Remove any frames that were part of the animation but are now before
+ // the animation.
+ if (ape.associatedEvents !== undefined) {
+ ape.associatedEvents = ape.associatedEvents.filter(
+ e => (!isImplFrameEvent(e) || e.start >= ape.start));
+ }
+ });
+ });
+ return protoExpectations;
+ }
+
+ function isImplFrameEvent(event) {
+ return event.title === tr.model.helpers.IMPL_RENDERING_STATS;
+ }
+
+ /**
+ * Merge Tap Responses that overlap Touch-only Animations.
+ * https: *github.com/catapult-project/catapult/issues/1431
+ */
+ function fixTapResponseTouchAnimations(protoExpectations) {
+ function isTapResponse(pe) {
+ return (pe.type === ProtoExpectation.RESPONSE_TYPE) &&
+ pe.containsTypeNames([INPUT_TYPE.TAP]);
+ }
+ function isTouchAnimation(pe) {
+ return (pe.type === ProtoExpectation.ANIMATION_TYPE) &&
+ pe.containsTypeNames([INPUT_TYPE.TOUCH_MOVE]) &&
+ !pe.containsTypeNames([
+ INPUT_TYPE.SCROLL_UPDATE, INPUT_TYPE.PINCH_UPDATE]);
+ }
+ const newPEs = [];
+ while (protoExpectations.length) {
+ const pe = protoExpectations.shift();
+ newPEs.push(pe);
+
+ // protoExpectations are sorted by start time, and we don't know whether
+ // the Tap Response or the Touch Animation will be first
+ const peIsTapResponse = isTapResponse(pe);
+ const peIsTouchAnimation = isTouchAnimation(pe);
+ if (!peIsTapResponse && !peIsTouchAnimation) {
+ continue;
+ }
+
+ for (let i = 0; i < protoExpectations.length; ++i) {
+ const otherPE = protoExpectations[i];
+
+ if (!otherPE.intersects(pe)) continue;
+
+ if (peIsTapResponse && !isTouchAnimation(otherPE)) continue;
+
+ if (peIsTouchAnimation && !isTapResponse(otherPE)) continue;
+
+ // pe might be the Touch Animation, but the merged ProtoExpectation
+ // should be a Response.
+ pe.type = ProtoExpectation.RESPONSE_TYPE;
+
+ pe.merge(otherPE);
+ protoExpectations.splice(i, 1);
+ // Don't skip the next otherPE!
+ --i;
+ }
+ }
+ return newPEs;
+ }
+
+ function findFrameEventsForAnimations(modelHelper, protoExpectations) {
+ const newPEs = [];
+ const frameEventsByPid = getSortedFrameEventsByProcess(modelHelper);
+
+ for (const pe of protoExpectations) {
+ if (pe.type !== ProtoExpectation.ANIMATION_TYPE) {
+ newPEs.push(pe);
+ continue;
+ }
+
+ const frameEvents = [];
+ for (const pid of Object.keys(modelHelper.rendererHelpers)) {
+ const range = tr.b.math.Range.fromExplicitRange(pe.start, pe.end);
+ frameEvents.push.apply(frameEvents,
+ range.filterArray(frameEventsByPid[pid], e => e.start));
+ }
+
+ // If a tree falls in a forest...
+ // If there were not actually any frames while the animation was
+ // running, then it wasn't really an animation, now, was it?
+ // Philosophy aside, the system_health Animation metrics fail hard if
+ // there are no frames in an AnimationExpectation.
+ // Since WebGL and VR animations don't generate this type of frame
+ // event, don't remove them if it's a WebGL or VR animation.
+ if (frameEvents.length === 0 &&
+ !(pe.initiatorType === INITIATOR_TYPE.WEBGL ||
+ pe.initiatorType === INITIATOR_TYPE.VR)) {
+ pe.type = ProtoExpectation.IGNORED_TYPE;
+ newPEs.push(pe);
+ continue;
+ }
+
+ pe.associatedEvents.addEventSet(frameEvents);
+ newPEs.push(pe);
+ }
+
+ return newPEs;
+ }
+
+ /**
+ * Check that none of the handlers accidentally ignored an input event.
+ */
+ function checkAllInputEventsHandled(
+ modelHelper, sortedInputEvents, protoExpectations, warn) {
+ const handledEvents = [];
+ protoExpectations.forEach(function(protoExpectation) {
+ protoExpectation.associatedEvents.forEach(function(event) {
+ // Ignore CSS Animations that might have multiple active ranges.
+ if ((event.title === CSS_ANIMATION_TITLE) &&
+ (event.subSlices.length > 0)) {
+ return;
+ }
+
+ if ((handledEvents.indexOf(event) >= 0) &&
+ (!isImplFrameEvent(event))) {
+ warn({
+ type: 'UserModelBuilder',
+ message: `double-handled event: ${event.typeName} @ ${event.start}`,
+ showToUser: false,
+ });
+ return;
+ }
+ handledEvents.push(event);
+ });
+ });
+
+ sortedInputEvents.forEach(function(event) {
+ if (handledEvents.indexOf(event) < 0) {
+ warn({
+ type: 'UserModelBuilder',
+ message: `double-handled event: ${event.typeName} @ ${event.start}`,
+ showToUser: false,
+ });
+ }
+ });
+ }
+
+ /**
+ * Find ProtoExpectations, post-process them, convert them to real UEs.
+ */
+ function findInputExpectations(modelHelper) {
+ // Prevent helper functions from producing too many import warnings.
+ let warning;
+ function warn(w) {
+ // Keep only the first warning.
+ if (warning) return;
+ warning = w;
+ }
+
+ const sortedInputEvents = getSortedInputEvents(modelHelper);
+ let protoExpectations = findProtoExpectations(
+ modelHelper, sortedInputEvents, warn);
+ protoExpectations = postProcessProtoExpectations(
+ modelHelper, protoExpectations);
+ checkAllInputEventsHandled(
+ modelHelper, sortedInputEvents, protoExpectations, warn);
+
+ if (warning) modelHelper.model.importWarning(warning);
+
+ const expectations = [];
+ protoExpectations.forEach(function(protoExpectation) {
+ const ir = protoExpectation.createInteractionRecord(modelHelper.model);
+ if (ir) {
+ expectations.push(ir);
+ }
+ });
+ return expectations;
+ }
+
+ return {
+ findInputExpectations,
+ compareEvents,
+ CSS_ANIMATION_TITLE,
+ };
+});
+</script>