blob: e9af77bd63b3137733d6f9143f818e58282ead2a [file] [log] [blame]
<!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="/base/base.html">
<link rel="import" href="/base/range_utils.html">
<link rel="import" href="/core/auditor.html">
<link rel="import" href="/extras/chrome/cc/input_latency_async_slice.html">
<link rel="import" href="/extras/chrome/chrome_model_helper.html">
<link rel="import" href="/extras/rail/idle_interaction_record.html">
<link rel="import" href="/extras/rail/load_interaction_record.html">
<link rel="import" href="/extras/rail/proto_ir.html">
<link rel="import" href="/model/event_info.html">
<script>
'use strict';
/**
* @fileoverview Base class for trace data Auditors.
*/
tr.exportTo('tr.e.rail', function() {
var INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES;
var ProtoIR = tr.e.rail.ProtoIR;
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;
}
// 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.
var INPUT_MERGE_THRESHOLD_MS = 200;
var ANIMATION_MERGE_THRESHOLD_MS = 1;
var INSIGNIFICANT_MS = 1;
var KEYBOARD_TYPE_NAMES = [
INPUT_TYPE.CHAR,
INPUT_TYPE.KEY_DOWN_RAW,
INPUT_TYPE.KEY_DOWN,
INPUT_TYPE.KEY_UP
];
var MOUSE_RESPONSE_TYPE_NAMES = [
INPUT_TYPE.MOUSE_WHEEL,
INPUT_TYPE.CLICK,
INPUT_TYPE.CONTEXT_MENU
];
var MOUSE_DRAG_TYPE_NAMES = [
INPUT_TYPE.MOUSE_DOWN,
INPUT_TYPE.MOUSE_MOVE,
INPUT_TYPE.MOUSE_UP
];
var TAP_TYPE_NAMES = [
INPUT_TYPE.TAP,
INPUT_TYPE.TAP_CANCEL,
INPUT_TYPE.TAP_DOWN
];
var PINCH_TYPE_NAMES = [
INPUT_TYPE.PINCH_BEGIN,
INPUT_TYPE.PINCH_END,
INPUT_TYPE.PINCH_UPDATE
];
var FLING_TYPE_NAMES = [
INPUT_TYPE.FLING_CANCEL,
INPUT_TYPE.FLING_START
];
var TOUCH_TYPE_NAMES = [
INPUT_TYPE.TOUCH_END,
INPUT_TYPE.TOUCH_MOVE,
INPUT_TYPE.TOUCH_START
];
var SCROLL_TYPE_NAMES = [
INPUT_TYPE.SCROLL_BEGIN,
INPUT_TYPE.SCROLL_END,
INPUT_TYPE.SCROLL_UPDATE
];
var ALL_HANDLED_TYPE_NAMES = [].concat(
KEYBOARD_TYPE_NAMES,
MOUSE_RESPONSE_TYPE_NAMES,
MOUSE_DRAG_TYPE_NAMES,
PINCH_TYPE_NAMES,
TAP_TYPE_NAMES,
FLING_TYPE_NAMES,
TOUCH_TYPE_NAMES,
SCROLL_TYPE_NAMES
);
var RENDERER_FLING_TITLE = 'InputHandlerProxy::HandleGestureFling::started';
function RAILIRFinder(model, modelHelper) {
this.model = model;
this.modelHelper = modelHelper;
};
RAILIRFinder.supportsModelHelper = function(modelHelper) {
return modelHelper.browserHelper !== undefined;
};
RAILIRFinder.prototype = {
findAllInteractionRecords: function() {
var rirs = [];
rirs.push.apply(rirs, this.findLoadInteractionRecords());
rirs.push.apply(rirs, this.findInputInteractionRecords());
// findIdleInteractionRecords must be called last!
rirs.push.apply(rirs, this.findIdleInteractionRecords(rirs));
return rirs;
},
// Fill in the empty space between IRs with IdleIRs.
findIdleInteractionRecords: function(otherIRs) {
if (this.model.bounds.isEmpty)
return;
var emptyRanges = tr.b.findEmptyRangesBetweenRanges(
tr.b.convertEventsToRanges(otherIRs),
this.model.bounds);
var irs = [];
emptyRanges.forEach(function(range) {
// Ignore insignificantly tiny idle ranges.
if (range.max < (range.min + INSIGNIFICANT_MS))
return;
irs.push(new tr.e.rail.IdleInteractionRecord(
range.min, range.max - range.min));
});
return irs;
},
getAllFrameEvents: function() {
var frameEvents = [];
frameEvents.push.apply(frameEvents,
this.modelHelper.browserHelper.getFrameEventsInRange(
tr.e.audits.IMPL_FRAMETIME_TYPE, this.model.bounds));
tr.b.iterItems(this.modelHelper.rendererHelpers, function(pid, renderer) {
frameEvents.push.apply(frameEvents, renderer.getFrameEventsInRange(
tr.e.audits.IMPL_FRAMETIME_TYPE, this.model.bounds));
}, this);
return frameEvents.sort(compareEvents);
},
getStartLoadEvents: function() {
function isStartLoadSlice(slice) {
return slice.title === 'RenderFrameImpl::didStartProvisionalLoad';
}
return this.modelHelper.browserHelper.getAllAsyncSlicesMatching(
isStartLoadSlice).sort(compareEvents);
},
getFailLoadEvents: function() {
function isFailLoadSlice(slice) {
return slice.title === 'RenderFrameImpl::didFailProvisionalLoad';
}
return this.modelHelper.browserHelper.getAllAsyncSlicesMatching(
isFailLoadSlice).sort(compareEvents);
},
// Match up RenderFrameImpl events with frame render events.
findLoadInteractionRecords: function() {
var commitLoadEvents =
this.modelHelper.browserHelper.getCommitProvisionalLoadEventsInRange(
this.model.bounds);
var frameEvents = this.getAllFrameEvents();
var startLoadEvents = this.getStartLoadEvents();
var failLoadEvents = this.getFailLoadEvents();
commitLoadEvents.forEach(function(commitLoad, commitLoadIndex) {
frameEvents.forEach(function(frameEvent) {
// Stop looking if this |commitLoad| already has its firstFrame.
if (commitLoad.firstFrame)
return;
// Ignore frame events before |commitLoad|.
if (frameEvent.start <= commitLoad.start)
return;
// Ignore frame events from different threads.
if (frameEvent.parentContainer.parent.pid !==
commitLoad.parentContainer.parent.pid)
return;
// Give up looking if the first frame event belongs to a different
// commitLoadEvent.
if (frameEvent.commitLoadEvent)
return;
// This appears to be the first frame event for this load, although
// this is just a heuristic.
commitLoad.firstFrame = frameEvent;
frameEvent.commitLoadEvent = commitLoad;
});
});
startLoadEvents.forEach(function(startLoad) {
failLoadEvents.forEach(function(failLoad) {
// Stop looking if this |startLoad| already has its |failLoadEvent|.
if (startLoad.failLoadEvent)
return;
// Ignore events from different threads.
if (startLoad.parentContainer.parent.pid !==
failLoad.parentContainer.parent.pid)
return;
// Ignore failLoad events before |startLoad|.
if (failLoad.start <= startLoad.start)
return;
// Give up looking if |failLoad| belongs to a different
// |startLoadEvent|.
if (failLoad.startLoadEvent)
return;
// This |failLoad| appears to go with |startLoad|, although this is
// just a heuristic.
failLoad.startLoadEvent = startLoad;
startLoad.failLoadEvent = failLoad;
});
});
var lirs = [];
commitLoadEvents.forEach(function(commitLoad, commitLoadIndex) {
if (!commitLoad.firstFrame)
return;
var lir = new tr.e.rail.LoadInteractionRecord(
commitLoad.start, commitLoad.firstFrame.end - commitLoad.start);
lir.associatedEvents.push(commitLoad);
lir.associatedEvents.push(commitLoad.firstFrame);
lirs.push(lir);
});
failLoadEvents.forEach(function(failLoad) {
if (!failLoad.startLoadEvent)
return;
var lir = new tr.e.rail.LoadInteractionRecord(
failLoad.startLoadEvent.start,
failLoad.start - failLoad.startLoadEvent.start);
lir.associatedEvents.push(failLoad.startLoadEvent);
lir.associatedEvents.push(failLoad);
lirs.push(lir);
});
return lirs;
},
// Find ProtoIRs, post-process them, convert them to real IRs.
findInputInteractionRecords: function() {
var protoIRs = this.findProtoIRs();
protoIRs = this.postProcessProtoIRs(protoIRs);
this.checkAllInputEventsHandled(protoIRs);
var irs = [];
protoIRs.forEach(function(protoIR) {
var ir = protoIR.createInteractionRecord();
if (ir)
irs.push(ir);
});
return irs;
},
findProtoIRs: function() {
var protoIRs = [];
// This order is not important. Handlers are independent.
protoIRs.push.apply(protoIRs, this.handleKeyboardEvents());
protoIRs.push.apply(protoIRs, this.handleMouseResponseEvents());
protoIRs.push.apply(protoIRs, this.handleMouseDragEvents());
protoIRs.push.apply(protoIRs, this.handleTapResponseEvents());
protoIRs.push.apply(protoIRs, this.handlePinchEvents());
protoIRs.push.apply(protoIRs, this.handleFlingEvents());
protoIRs.push.apply(protoIRs, this.handleTouchEvents());
protoIRs.push.apply(protoIRs, this.handleScrollEvents());
protoIRs.push.apply(protoIRs, this.handleCSSAnimations());
protoIRs.sort(compareEvents);
return protoIRs;
},
getSortedInputEvents: function(typeNames) {
function isMatchingSlice(slice) {
if (!slice.isTopLevel)
return false;
if (!(slice instanceof tr.e.cc.InputLatencyAsyncSlice))
return false;
return typeNames.indexOf(slice.typeName) >= 0;
}
return this.modelHelper.browserHelper.getAllAsyncSlicesMatching(
isMatchingSlice).sort(compareEvents);
},
// Every keyboard event is a Response.
handleKeyboardEvents: function() {
var protoIRs = [];
this.getSortedInputEvents(KEYBOARD_TYPE_NAMES).forEach(function(event) {
var pir = new ProtoIR(ProtoIR.RESPONSE_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
});
return protoIRs;
},
// Some mouse events can be translated directly into Responses.
handleMouseResponseEvents: function() {
var protoIRs = [];
this.getSortedInputEvents(MOUSE_RESPONSE_TYPE_NAMES).forEach(
function(event) {
var pir = new ProtoIR(ProtoIR.RESPONSE_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
});
return protoIRs;
},
// 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
//
handleMouseDragEvents: function() {
var protoIRs = [];
var currentPIR = undefined;
var moveCount = 0;
var mouseDownEvent = undefined;
this.getSortedInputEvents(MOUSE_DRAG_TYPE_NAMES).forEach(function(event) {
switch (event.typeName) {
case INPUT_TYPE.MOUSE_DOWN:
// Responses typically don't start until the mouse up event.
mouseDownEvent = event;
moveCount = 0;
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. ResponseIRs start scoring "pain" at
// 100ms duration. If more than that 100ms duration is burned
// through while waiting for the user to release the
// mouse button, then ResponseIR will unfairly start scoring pain
// 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 (!mouseDownEvent) {
var pir = new ProtoIR(ProtoIR.IGNORED_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
break;
}
moveCount++;
if (moveCount === 1) {
currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE);
currentPIR.pushEvent(event);
currentPIR.associatedEvents.push(mouseDownEvent);
protoIRs.push(currentPIR);
} else if (moveCount === 2) {
currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
} else {
currentPIR.pushEvent(event);
}
break;
case INPUT_TYPE.MOUSE_UP:
if (!mouseDownEvent) {
var pir = new ProtoIR(ProtoIR.IGNORED_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
break;
}
if (currentPIR) {
currentPIR.pushEvent(event);
} else {
currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE);
currentPIR.associatedEvents.push(mouseDownEvent);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
}
mouseDownEvent = undefined;
moveCount = 0;
currentPIR = undefined;
break;
}
});
if (mouseDownEvent) {
currentPIR = new ProtoIR(ProtoIR.IGNORED_TYPE);
currentPIR.pushEvent(mouseDownEvent);
protoIRs.push(currentPIR);
}
return protoIRs;
},
// 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 IRs
// if possible.
//
// RRRRRRR
// DDD CCC
//
handleTapResponseEvents: function() {
var protoIRs = [];
var currentPIR = undefined;
this.getSortedInputEvents(TAP_TYPE_NAMES).forEach(function(event) {
switch (event.typeName) {
case INPUT_TYPE.TAP_DOWN:
currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
break;
case INPUT_TYPE.TAP:
if (currentPIR) {
currentPIR.pushEvent(event);
} else {
// Sometimes we get Tap events with no TapDown, sometimes we get
// TapDown events. Handle both.
currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
}
currentPIR = undefined;
break;
case INPUT_TYPE.TAP_CANCEL:
if (!currentPIR) {
var pir = new ProtoIR(ProtoIR.IGNORED_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
break;
}
if (currentPIR.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
currentPIR.pushEvent(event);
} else {
currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
}
currentPIR = undefined;
break;
}
});
return protoIRs;
},
// 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
//
handlePinchEvents: function() {
var protoIRs = [];
var currentPIR = undefined;
var sawFirstUpdate = false;
var modelBounds = this.model.bounds;
this.getSortedInputEvents(PINCH_TYPE_NAMES).forEach(function(event) {
switch (event.typeName) {
case INPUT_TYPE.PINCH_BEGIN:
if (currentPIR &&
currentPIR.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
currentPIR.pushEvent(event);
break;
}
currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
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 (!currentPIR ||
((currentPIR.irType === ProtoIR.RESPONSE_TYPE) &&
sawFirstUpdate) ||
!currentPIR.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
} else {
currentPIR.pushEvent(event);
sawFirstUpdate = true;
}
break;
case INPUT_TYPE.PINCH_END:
if (currentPIR) {
currentPIR.pushEvent(event);
} else {
var pir = new ProtoIR(ProtoIR.IGNORED_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
}
currentPIR = undefined;
break;
}
});
return protoIRs;
},
// 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
//
handleFlingEvents: function() {
var protoIRs = [];
var currentPIR = undefined;
var flingEvents = this.getSortedInputEvents(FLING_TYPE_NAMES);
function isRendererFling(event) {
return event.title === RENDERER_FLING_TITLE;
}
var browserHelper = this.modelHelper.browserHelper;
flingEvents.push.apply(flingEvents,
browserHelper.getAllAsyncSlicesMatching(isRendererFling));
flingEvents.forEach(function(event) {
if (event.title === RENDERER_FLING_TITLE) {
if (currentPIR) {
currentPIR.pushEvent(event);
} else {
currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
}
return;
}
switch (event.typeName) {
case INPUT_TYPE.FLING_START:
if (currentPIR) {
console.error('Another FlingStart? File a bug with this trace!');
currentPIR.pushEvent(event);
} else {
currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
currentPIR.pushEvent(event);
// Set end to an invalid value so that it can be noticed and fixed
// later.
currentPIR.end = 0;
protoIRs.push(currentPIR);
}
break;
case INPUT_TYPE.FLING_CANCEL:
if (currentPIR) {
currentPIR.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 IRs look more correct if they don't
// overlap unnecessarily.
currentPIR.end = event.start;
currentPIR = undefined;
} else {
var pir = new ProtoIR(ProtoIR.IGNORED_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
}
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 currentPIR to the end of the model.
if (currentPIR && !currentPIR.end)
currentPIR.end = this.model.bounds.max;
return protoIRs;
},
// 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
//
handleTouchEvents: function() {
var protoIRs = [];
var currentPIR = undefined;
var sawFirstMove = false;
this.getSortedInputEvents(TOUCH_TYPE_NAMES).forEach(function(event) {
switch (event.typeName) {
case INPUT_TYPE.TOUCH_START:
if (currentPIR) {
// NB: currentPIR 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.
currentPIR.pushEvent(event);
} else {
currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
sawFirstMove = false;
}
break;
case INPUT_TYPE.TOUCH_MOVE:
if (!currentPIR) {
currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
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 &&
(currentPIR.irType === ProtoIR.RESPONSE_TYPE)) ||
!currentPIR.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
// If there's already a touchmove in the currentPIR or it's not
// near event, then finish it and start a new animation.
var prevEnd = currentPIR.end;
currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
currentPIR.pushEvent(event);
// It's possible for there to be a gap between TouchMoves, but
// that doesn't mean that there should be an Idle IR there.
currentPIR.start = prevEnd;
protoIRs.push(currentPIR);
} else {
currentPIR.pushEvent(event);
sawFirstMove = true;
}
break;
case INPUT_TYPE.TOUCH_END:
if (!currentPIR) {
var pir = new ProtoIR(ProtoIR.IGNORED_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
break;
}
if (currentPIR.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
currentPIR.pushEvent(event);
} else {
var pir = new ProtoIR(ProtoIR.IGNORED_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
}
currentPIR = undefined;
break;
}
});
return protoIRs;
},
// The first ScrollBegin and the first ScrollUpdate comprise a Response,
// then the rest comprise an Animation.
//
// RRRRRRRAAAAAAAAAAAAAAAAAAAA
// BBB UUU UUU UUU UUU UUU EEE
//
handleScrollEvents: function() {
var protoIRs = [];
var currentPIR = undefined;
var sawFirstUpdate = false;
this.getSortedInputEvents(SCROLL_TYPE_NAMES).forEach(function(event) {
switch (event.typeName) {
case INPUT_TYPE.SCROLL_BEGIN:
// Always begin a new PIR even if there already is one, unlike
// PinchBegin.
currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
sawFirstUpdate = false;
break;
case INPUT_TYPE.SCROLL_UPDATE:
if (currentPIR) {
if (currentPIR.isNear(event, INPUT_MERGE_THRESHOLD_MS) &&
((currentPIR.irType === ProtoIR.ANIMATION_TYPE) ||
!sawFirstUpdate)) {
currentPIR.pushEvent(event);
sawFirstUpdate = true;
} else {
currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
}
} else {
// ScrollUpdate without ScrollBegin.
currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
currentPIR.pushEvent(event);
protoIRs.push(currentPIR);
}
break;
case INPUT_TYPE.SCROLL_END:
if (!currentPIR) {
console.error('ScrollEnd without ScrollUpdate? ' +
'File a bug with this trace!');
var pir = new ProtoIR(ProtoIR.IGNORED_TYPE);
pir.pushEvent(event);
protoIRs.push(pir);
break;
}
currentPIR.pushEvent(event);
break;
}
});
return protoIRs;
},
// CSS Animations are merged into Animations when they intersect.
handleCSSAnimations: function() {
var animationEvents = this.modelHelper.browserHelper.
getAllAsyncSlicesMatching(function(event) {
return event.title === 'Animation';
});
var animationRanges = [];
animationEvents.forEach(function(event) {
animationRanges.push({
min: event.start,
max: event.end,
event: event
});
});
function merge(ranges) {
var protoIR = new ProtoIR(ProtoIR.ANIMATION_TYPE);
ranges.forEach(function(range) {
protoIR.pushEvent(range.event);
});
return protoIR;
}
return tr.b.mergeRanges(animationRanges,
ANIMATION_MERGE_THRESHOLD_MS,
merge);
},
postProcessProtoIRs: function(protoIRs) {
// protoIRs is input only. Returns a modified set of ProtoIRs.
// The order is important.
protoIRs = this.mergeIntersectingResponses(protoIRs);
protoIRs = this.mergeIntersectingAnimations(protoIRs);
protoIRs = this.fixResponseAnimationStarts(protoIRs);
return protoIRs;
},
// 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' PIRs.
//
// For example:
// RR
// RRR -> RRRRR
// RR
//
// protoIRs is input only.
// Returns a modified set of ProtoIRs.
mergeIntersectingResponses: function(protoIRs) {
var newPIRs = [];
while (protoIRs.length) {
var pir = protoIRs.shift();
newPIRs.push(pir);
// Only consider Responses for now.
if (pir.irType !== ProtoIR.RESPONSE_TYPE)
continue;
for (var i = 0; i < protoIRs.length; ++i) {
var otherPIR = protoIRs[i];
if (otherPIR.irType !== pir.irType)
continue;
if (!otherPIR.intersects(pir))
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.
var typeNames = pir.associatedEvents.map(function(event) {
return event.typeName;
});
if (otherPIR.containsTypeNames(typeNames))
continue;
pir.merge(otherPIR);
protoIRs.splice(i, 1);
// Don't skip the next otherPIR!
--i;
}
}
return newPIRs;
},
// 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
//
// protoIRs is input only.
// Returns a modified set of ProtoIRs.
mergeIntersectingAnimations: function(protoIRs) {
var newPIRs = [];
while (protoIRs.length) {
var pir = protoIRs.shift();
newPIRs.push(pir);
// Only consider Animations for now.
if (pir.irType !== ProtoIR.ANIMATION_TYPE)
continue;
for (var i = 0; i < protoIRs.length; ++i) {
var otherPIR = protoIRs[i];
if (otherPIR.irType !== pir.irType)
continue;
if (!otherPIR.intersects(pir))
continue;
pir.merge(otherPIR);
protoIRs.splice(i, 1);
// Don't skip the next otherPIR!
--i;
}
}
return newPIRs;
},
// 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
//
// protoIRs is input only.
// Returns a modified set of ProtoIRs.
fixResponseAnimationStarts: function(protoIRs) {
protoIRs.forEach(function(apir) {
// Only consider animations for now.
if (apir.irType !== ProtoIR.ANIMATION_TYPE)
return;
protoIRs.forEach(function(rpir) {
// Only consider responses for now.
if (rpir.irType !== ProtoIR.RESPONSE_TYPE)
return;
// Only consider responses that end during the animation.
if (!apir.containsTimestampInclusive(rpir.end))
return;
// Ignore Responses that are entirely contained by the animation.
if (apir.containsTimestampInclusive(rpir.start))
return;
// Move the animation start to the response end.
apir.start = rpir.end;
});
});
return protoIRs;
},
// Check that none of the handlers accidentally ignored an input event.
checkAllInputEventsHandled: function(protoIRs) {
var handledEvents = [];
protoIRs.forEach(function(protoIR) {
protoIR.associatedEvents.forEach(function(event) {
if (handledEvents.indexOf(event) >= 0) {
console.error('double-handled event', event.typeName,
parseInt(event.start), parseInt(event.end), protoIR);
return;
}
handledEvents.push(event);
});
});
this.getSortedInputEvents(ALL_HANDLED_TYPE_NAMES).forEach(
function(event) {
if (handledEvents.indexOf(event) < 0) {
console.error('UNHANDLED INPUT EVENT!',
event.typeName, parseInt(event.start), parseInt(event.end));
}
});
}
};
return {
RAILIRFinder: RAILIRFinder
};
});
</script>