blob: cd2aff601f62263653f5659dd959736c004ad556 [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="/tracing/base/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">
<script>
'use strict';
tr.exportTo('tr.importer', function() {
var ProtoExpectation = tr.importer.ProtoExpectation;
var INPUT_TYPE = tr.e.cc.INPUT_EVENT_TYPE_NAMES;
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.CLICK,
INPUT_TYPE.CONTEXT_MENU
];
var MOUSE_WHEEL_TYPE_NAMES = [
INPUT_TYPE.MOUSE_WHEEL
];
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_WHEEL_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';
var PLAYBACK_EVENT_TITLE = 'VideoPlayback';
var CSS_ANIMATION_TITLE = 'Animation';
/**
* 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 = 32; // 2x 60FPS frames
/**
* If two MouseWheel events begin this close together, then they're an
* Animation, not two responses.
*/
var MOUSE_WHEEL_THRESHOLD_MS = 40;
/**
* If two MouseMoves are more than this far apart, then they're two Responses,
* not Animation.
*/
var MOUSE_MOVE_THRESHOLD_MS = 40;
// Strings used to name IRs.
var KEYBOARD_IR_NAME = 'Keyboard';
var MOUSE_IR_NAME = 'Mouse';
var MOUSEWHEEL_IR_NAME = 'MouseWheel';
var TAP_IR_NAME = 'Tap';
var PINCH_IR_NAME = 'Pinch';
var FLING_IR_NAME = 'Fling';
var TOUCH_IR_NAME = 'Touch';
var SCROLL_IR_NAME = 'Scroll';
var CSS_IR_NAME = 'CSS';
var WEBGL_IR_NAME = 'WebGL';
var VIDEO_IR_NAME = 'Video';
// TODO(benjhayden) Find a better home for 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(
x => x.title === tr.model.helpers.IMPL_RENDERING_STATS);
}
function getSortedFrameEventsByProcess(modelHelper) {
var frameEventsByPid = {};
tr.b.iterItems(modelHelper.rendererHelpers, function(pid, rendererHelper) {
frameEventsByPid[pid] = rendererHelper.getFrameEventsInRange(
tr.model.helpers.IMPL_FRAMETIME_TYPE, modelHelper.model.bounds);
});
return frameEventsByPid;
}
function getSortedInputEvents(modelHelper) {
var inputEvents = [];
var browserProcess = modelHelper.browserHelper.process;
var mainThread = browserProcess.findAtMostOneThreadNamed(
'CrBrowserMain');
for (var slice of mainThread.asyncSliceGroup.getDescendantEvents()) {
if (!slice.isTopLevel)
continue;
if (!(slice instanceof tr.e.cc.InputLatencyAsyncSlice))
continue;
// TODO(beaudoin): This should never happen but it does. Investigate
// the trace linked at in #1567 and remove that when it's fixed.
if (isNaN(slice.start) ||
isNaN(slice.duration) ||
isNaN(slice.end))
continue;
inputEvents.push(slice);
}
return inputEvents.sort(compareEvents);
}
function findProtoExpectations(modelHelper, sortedInputEvents) {
var protoExpectations = [];
// This order is not important. Handlers are independent.
var handlers = [
handleKeyboardEvents,
handleMouseResponseEvents,
handleMouseWheelEvents,
handleMouseDragEvents,
handleTapResponseEvents,
handlePinchEvents,
handleFlingEvents,
handleTouchEvents,
handleScrollEvents,
handleCSSAnimations,
handleWebGLAnimations,
handleVideoAnimations
];
handlers.forEach(function(handler) {
protoExpectations.push.apply(protoExpectations, handler(
modelHelper, sortedInputEvents));
});
protoExpectations.sort(compareEvents);
return protoExpectations;
}
/**
* Every keyboard event is a Response.
*/
function handleKeyboardEvents(modelHelper, sortedInputEvents) {
var protoExpectations = [];
forEventTypesIn(sortedInputEvents, KEYBOARD_TYPE_NAMES, function(event) {
var pe = new ProtoExpectation(
ProtoExpectation.RESPONSE_TYPE, KEYBOARD_IR_NAME);
pe.pushEvent(event);
protoExpectations.push(pe);
});
return protoExpectations;
}
/**
* Some mouse events can be translated directly into Responses.
*/
function handleMouseResponseEvents(modelHelper, sortedInputEvents) {
var protoExpectations = [];
forEventTypesIn(
sortedInputEvents, MOUSE_RESPONSE_TYPE_NAMES, function(event) {
var pe = new ProtoExpectation(
ProtoExpectation.RESPONSE_TYPE, MOUSE_IR_NAME);
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
* IRs. 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) {
var protoExpectations = [];
var currentPE = undefined;
var prevEvent_ = undefined;
forEventTypesIn(
sortedInputEvents, MOUSE_WHEEL_TYPE_NAMES, function(event) {
// Switch prevEvent in one place so that we can early-return later.
var prevEvent = prevEvent_;
prevEvent_ = event;
if (currentPE &&
(prevEvent.start + MOUSE_WHEEL_THRESHOLD_MS) >= event.start) {
if (currentPE.irType === ProtoExpectation.ANIMATION_TYPE) {
currentPE.pushEvent(event);
} else {
currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
MOUSEWHEEL_IR_NAME);
currentPE.pushEvent(event);
protoExpectations.push(currentPE);
}
return;
}
currentPE = new ProtoExpectation(
ProtoExpectation.RESPONSE_TYPE, MOUSEWHEEL_IR_NAME);
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) {
var protoExpectations = [];
var currentPE = undefined;
var mouseDownEvent = undefined;
forEventTypesIn(
sortedInputEvents, MOUSE_DRAG_TYPE_NAMES, function(event) {
switch (event.typeName) {
case INPUT_TYPE.MOUSE_DOWN:
if (causedFrame(event)) {
var pe = new ProtoExpectation(
ProtoExpectation.RESPONSE_TYPE, MOUSE_IR_NAME);
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. ResponseIRs 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 ResponseIR 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.
var 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, MOUSE_IR_NAME);
currentPE.pushEvent(event);
if (mouseDownEvent) {
currentPE.associatedEvents.push(mouseDownEvent);
mouseDownEvent = undefined;
}
protoExpectations.push(currentPE);
} else {
// Merge this event into an Animation.
if (currentPE.irType === ProtoExpectation.ANIMATION_TYPE) {
currentPE.pushEvent(event);
} else {
currentPE = new ProtoExpectation(
ProtoExpectation.ANIMATION_TYPE, MOUSE_IR_NAME);
currentPE.pushEvent(event);
protoExpectations.push(currentPE);
}
}
break;
case INPUT_TYPE.MOUSE_UP:
if (!mouseDownEvent) {
var pe = new ProtoExpectation(
causedFrame(event) ? ProtoExpectation.RESPONSE_TYPE :
ProtoExpectation.IGNORED_TYPE,
MOUSE_IR_NAME);
pe.pushEvent(event);
protoExpectations.push(pe);
break;
}
if (currentPE) {
currentPE.pushEvent(event);
} else {
currentPE = new ProtoExpectation(
ProtoExpectation.RESPONSE_TYPE, MOUSE_IR_NAME);
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 IRs
* if possible.
*
* RRRRRRR
* DDD CCC
**/
function handleTapResponseEvents(modelHelper, sortedInputEvents) {
var protoExpectations = [];
var currentPE = undefined;
forEventTypesIn(sortedInputEvents, TAP_TYPE_NAMES, function(event) {
switch (event.typeName) {
case INPUT_TYPE.TAP_DOWN:
currentPE = new ProtoExpectation(
ProtoExpectation.RESPONSE_TYPE, TAP_IR_NAME);
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, TAP_IR_NAME);
currentPE.pushEvent(event);
protoExpectations.push(currentPE);
}
currentPE = undefined;
break;
case INPUT_TYPE.TAP_CANCEL:
if (!currentPE) {
var 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, TAP_IR_NAME);
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) {
var protoExpectations = [];
var currentPE = undefined;
var sawFirstUpdate = false;
var 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, PINCH_IR_NAME);
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.irType === ProtoExpectation.RESPONSE_TYPE) &&
sawFirstUpdate) ||
!currentPE.isNear(event, INPUT_MERGE_THRESHOLD_MS)) {
currentPE = new ProtoExpectation(
ProtoExpectation.ANIMATION_TYPE, PINCH_IR_NAME);
currentPE.pushEvent(event);
protoExpectations.push(currentPE);
} else {
currentPE.pushEvent(event);
sawFirstUpdate = true;
}
break;
case INPUT_TYPE.PINCH_END:
if (currentPE) {
currentPE.pushEvent(event);
} else {
var 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) {
var protoExpectations = [];
var currentPE = undefined;
function isRendererFling(event) {
return event.title === RENDERER_FLING_TITLE;
}
var browserHelper = modelHelper.browserHelper;
var 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, FLING_IR_NAME);
currentPE.pushEvent(event);
protoExpectations.push(currentPE);
}
return;
}
switch (event.typeName) {
case INPUT_TYPE.FLING_START:
if (currentPE) {
console.error('Another FlingStart? File a bug with this trace!');
currentPE.pushEvent(event);
} else {
currentPE = new ProtoExpectation(
ProtoExpectation.ANIMATION_TYPE, FLING_IR_NAME);
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 IRs look more correct if they don't
// overlap unnecessarily.
currentPE.end = event.start;
currentPE = undefined;
} else {
var 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) {
var protoExpectations = [];
var currentPE = undefined;
var 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, TOUCH_IR_NAME);
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, TOUCH_IR_NAME);
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.irType === 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.
var prevEnd = currentPE.end;
currentPE = new ProtoExpectation(
ProtoExpectation.ANIMATION_TYPE, TOUCH_IR_NAME);
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 IR there.
currentPE.start = prevEnd;
protoExpectations.push(currentPE);
} else {
currentPE.pushEvent(event);
sawFirstMove = true;
}
break;
case INPUT_TYPE.TOUCH_END:
if (!currentPE) {
var 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 {
var 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) {
var protoExpectations = [];
var currentPE = undefined;
var 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, SCROLL_IR_NAME);
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.irType === ProtoExpectation.ANIMATION_TYPE) ||
!sawFirstUpdate)) {
currentPE.pushEvent(event);
sawFirstUpdate = true;
} else {
currentPE = new ProtoExpectation(ProtoExpectation.ANIMATION_TYPE,
SCROLL_IR_NAME);
currentPE.pushEvent(event);
protoExpectations.push(currentPE);
}
} else {
// ScrollUpdate without ScrollBegin.
currentPE = new ProtoExpectation(
ProtoExpectation.ANIMATION_TYPE, SCROLL_IR_NAME);
currentPE.pushEvent(event);
protoExpectations.push(currentPE);
}
break;
case INPUT_TYPE.SCROLL_END:
if (!currentPE) {
console.error('ScrollEnd without ScrollUpdate? ' +
'File a bug with this trace!');
var 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) {
var events = [];
for (var pid in modelHelper.rendererHelpers) {
for (var asyncSlice of
modelHelper.rendererHelpers[pid].mainThread.asyncSliceGroup.slices) {
if (asyncSlice.title === PLAYBACK_EVENT_TITLE)
events.push(asyncSlice);
}
}
events.sort(tr.importer.compareEvents);
var protoExpectations = [];
for (var event of events) {
var currentPE = new ProtoExpectation(
ProtoExpectation.ANIMATION_TYPE, VIDEO_IR_NAME);
currentPE.start = event.start;
currentPE.end = event.end;
currentPE.pushEvent(event);
protoExpectations.push(currentPE);
}
return protoExpectations;
}
/**
* CSS Animations are merged into AnimationExpectations when they intersect.
*/
function handleCSSAnimations(modelHelper, sortedInputEvents) {
// First find all the top-level CSS Animation async events.
var 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}.
var 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) {
var range = tr.b.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.
var 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) {
var protoExpectation = new ProtoExpectation(
ProtoExpectation.ANIMATION_TYPE, CSS_IR_NAME);
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 (var 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);
var animationIterator = animationEvents[Symbol.iterator]();
var animationEvent = animationIterator.next().value;
var 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 (var 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) {
var protoExpectations = [];
var currentPE = undefined;
for (var event of mailboxEvents) {
if (currentPE === undefined || !currentPE.isNear(
event, ANIMATION_MERGE_THRESHOLD_MS)) {
currentPE = new ProtoExpectation(
ProtoExpectation.ANIMATION_TYPE, WEBGL_IR_NAME);
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) {
// Get the prepareMailbox and scriptedAnimation events.
var prepareMailboxEvents = [];
var scriptedAnimationEvents = [];
findWebGLEvents(modelHelper, prepareMailboxEvents, scriptedAnimationEvents);
var 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) {
var newPEs = [];
while (protoExpectations.length) {
var pe = protoExpectations.shift();
newPEs.push(pe);
// Only consider Responses for now.
if (pe.irType !== ProtoExpectation.RESPONSE_TYPE)
continue;
for (var i = 0; i < protoExpectations.length; ++i) {
var otherPE = protoExpectations[i];
if (otherPE.irType !== pe.irType)
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.
var 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) {
var newPEs = [];
while (protoExpectations.length) {
var pe = protoExpectations.shift();
newPEs.push(pe);
// Only consider Animations for now.
if (pe.irType !== ProtoExpectation.ANIMATION_TYPE)
continue;
var isCSS = pe.containsSliceTitle(CSS_ANIMATION_TITLE);
var isFling = pe.containsTypeNames([INPUT_TYPE.FLING_START]);
var isVideo = pe.containsTypeNames([VIDEO_IR_NAME]);
for (var i = 0; i < protoExpectations.length; ++i) {
var otherPE = protoExpectations[i];
if (otherPE.irType !== pe.irType)
continue;
// Don't merge CSS Animations with any other types.
if (isCSS != otherPE.containsSliceTitle(CSS_ANIMATION_TITLE))
continue;
if (isCSS) {
if (!pe.isNear(otherPE, ANIMATION_MERGE_THRESHOLD_MS))
continue;
} else if (!otherPE.intersects(pe)) {
continue;
}
// Don't merge Fling Animations with any other types.
if (isFling !== otherPE.containsTypeNames([INPUT_TYPE.FLING_START]))
continue;
// Don't merge Video Animations with any other types.
if (isVideo !== otherPE.containsTypeNames([VIDEO_IR_NAME]))
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.irType !== ProtoExpectation.ANIMATION_TYPE)
return;
protoExpectations.forEach(function(rpe) {
// Only consider responses for now.
if (rpe.irType !== 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;
});
});
return protoExpectations;
}
/**
* Merge Tap Responses that overlap Touch-only Animations.
* https: *github.com/catapult-project/catapult/issues/1431
*/
function fixTapResponseTouchAnimations(protoExpectations) {
function isTapResponse(pe) {
return (pe.irType === ProtoExpectation.RESPONSE_TYPE) &&
pe.containsTypeNames([INPUT_TYPE.TAP]);
}
function isTouchAnimation(pe) {
return (pe.irType === ProtoExpectation.ANIMATION_TYPE) &&
pe.containsTypeNames([INPUT_TYPE.TOUCH_MOVE]) &&
!pe.containsTypeNames([
INPUT_TYPE.SCROLL_UPDATE, INPUT_TYPE.PINCH_UPDATE]);
}
var newPEs = [];
while (protoExpectations.length) {
var 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
var peIsTapResponse = isTapResponse(pe);
var peIsTouchAnimation = isTouchAnimation(pe);
if (!peIsTapResponse && !peIsTouchAnimation)
continue;
for (var i = 0; i < protoExpectations.length; ++i) {
var 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.irType = ProtoExpectation.RESPONSE_TYPE;
pe.merge(otherPE);
protoExpectations.splice(i, 1);
// Don't skip the next otherPE!
--i;
}
}
return newPEs;
}
function findFrameEventsForAnimations(modelHelper, protoExpectations) {
var newPEs = [];
var frameEventsByPid = getSortedFrameEventsByProcess(modelHelper);
for (var pe of protoExpectations) {
if (pe.irType !== ProtoExpectation.ANIMATION_TYPE) {
newPEs.push(pe);
continue;
}
var frameEvents = [];
// TODO(benjhayden): Use frame blame contexts here.
for (var pid of Object.keys(modelHelper.rendererHelpers)) {
var range = tr.b.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 animations don't generate this type of frame event,
// don't remove them if it's a WebGL animation.
// TODO(alexandermont): Identify what the correct frame event to
// use here is.
if (frameEvents.length === 0 && !pe.names.has(WEBGL_IR_NAME)) {
pe.irType = 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(sortedInputEvents, protoExpectations) {
var 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) &&
(event.title !== tr.model.helpers.IMPL_RENDERING_STATS)) {
console.error('double-handled event', event.typeName,
parseInt(event.start), parseInt(event.end), protoExpectation);
return;
}
handledEvents.push(event);
});
});
sortedInputEvents.forEach(function(event) {
if (handledEvents.indexOf(event) < 0) {
console.error('UNHANDLED INPUT EVENT!',
event.typeName, parseInt(event.start), parseInt(event.end));
}
});
}
/**
* Find ProtoExpectations, post-process them, convert them to real IRs.
*/
function findInputExpectations(modelHelper) {
var sortedInputEvents = getSortedInputEvents(modelHelper);
var protoExpectations = findProtoExpectations(
modelHelper, sortedInputEvents);
protoExpectations = postProcessProtoExpectations(
modelHelper, protoExpectations);
checkAllInputEventsHandled(sortedInputEvents, protoExpectations);
var irs = [];
protoExpectations.forEach(function(protoExpectation) {
var ir = protoExpectation.createInteractionRecord(modelHelper.model);
if (ir)
irs.push(ir);
});
return irs;
}
return {
findInputExpectations: findInputExpectations,
compareEvents: compareEvents,
CSS_ANIMATION_TITLE: CSS_ANIMATION_TITLE
};
});
</script>