blob: ce1e65120b6274513cc17ce3ef032d956a999c73 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright (c) 2013 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/extras/chrome/chrome_model_helper.html">
<link rel="import" href="/tracing/model/async_slice.html">
<link rel="import" href="/tracing/model/event_set.html">
<script>
'use strict';
tr.exportTo('tr.e.cc', function() {
var AsyncSlice = tr.model.AsyncSlice;
var EventSet = tr.model.EventSet;
var UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT';
var ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT';
var BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT';
var END_COMP_NAME = 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT';
var MAIN_RENDERER_THREAD_NAME = 'CrRendererMain';
var COMPOSITOR_THREAD_NAME = 'Compositor';
var POSTTASK_FLOW_EVENT = 'disabled-by-default-toplevel.flow';
var IPC_FLOW_EVENT = 'disabled-by-default-ipc.flow';
var INPUT_EVENT_TYPE_NAMES = {
CHAR: 'Char',
CLICK: 'GestureClick',
CONTEXT_MENU: 'ContextMenu',
FLING_CANCEL: 'GestureFlingCancel',
FLING_START: 'GestureFlingStart',
KEY_DOWN: 'KeyDown',
KEY_DOWN_RAW: 'RawKeyDown',
KEY_UP: 'KeyUp',
LATENCY_SCROLL_UPDATE: 'ScrollUpdate',
MOUSE_DOWN: 'MouseDown',
MOUSE_ENTER: 'MouseEnter',
MOUSE_LEAVE: 'MouseLeave',
MOUSE_MOVE: 'MouseMove',
MOUSE_UP: 'MouseUp',
MOUSE_WHEEL: 'MouseWheel',
PINCH_BEGIN: 'GesturePinchBegin',
PINCH_END: 'GesturePinchEnd',
PINCH_UPDATE: 'GesturePinchUpdate',
SCROLL_BEGIN: 'GestureScrollBegin',
SCROLL_END: 'GestureScrollEnd',
SCROLL_UPDATE: 'GestureScrollUpdate',
SCROLL_UPDATE_RENDERER: 'ScrollUpdate',
SHOW_PRESS: 'GestureShowPress',
TAP: 'GestureTap',
TAP_CANCEL: 'GestureTapCancel',
TAP_DOWN: 'GestureTapDown',
TOUCH_CANCEL: 'TouchCancel',
TOUCH_END: 'TouchEnd',
TOUCH_MOVE: 'TouchMove',
TOUCH_START: 'TouchStart',
UNKNOWN: 'UNKNOWN'
};
function InputLatencyAsyncSlice() {
AsyncSlice.apply(this, arguments);
this.associatedEvents_ = new EventSet();
this.typeName_ = undefined;
if (!this.isLegacyEvent)
this.determineModernTypeName_();
}
InputLatencyAsyncSlice.prototype = {
__proto__: AsyncSlice.prototype,
// Legacy InputLatencyAsyncSlices involve a top-level slice titled
// "InputLatency" containing a subSlice whose title starts with
// "InputLatency:". Modern InputLatencyAsyncSlices involve a single
// top-level slice whose title starts with "InputLatency::".
// Legacy subSlices are not available at construction time, so
// determineLegacyTypeName_() must be called at get time.
// So this returns false for the legacy subSlice events titled like
// "InputLatency:Foo" even though they are technically legacy events.
get isLegacyEvent() {
return this.title === 'InputLatency';
},
get typeName() {
if (!this.typeName_)
this.determineLegacyTypeName_();
return this.typeName_;
},
checkTypeName_: function() {
if (!this.typeName_)
throw 'Unable to determine typeName';
var found = false;
for (var type_name in INPUT_EVENT_TYPE_NAMES) {
if (this.typeName === INPUT_EVENT_TYPE_NAMES[type_name]) {
found = true;
break;
}
}
if (!found)
this.typeName_ = INPUT_EVENT_TYPE_NAMES.UNKNOWN;
},
determineModernTypeName_: function() {
// This method works both on modern events titled like
// "InputLatency::Foo" and also on the legacy subSlices titled like
// "InputLatency:Foo". Modern events' titles contain 2 colons, whereas the
// legacy subSlices events contain 1 colon.
var lastColonIndex = this.title.lastIndexOf(':');
if (lastColonIndex < 0)
return;
var characterAfterLastColonIndex = lastColonIndex + 1;
this.typeName_ = this.title.slice(characterAfterLastColonIndex);
// Check that the determined typeName is known.
this.checkTypeName_();
},
determineLegacyTypeName_: function() {
// Iterate over all descendent subSlices.
this.iterateAllDescendents(function(subSlice) {
// If |subSlice| is not an InputLatencyAsyncSlice, then ignore it.
var subSliceIsAInputLatencyAsyncSlice = (
subSlice instanceof InputLatencyAsyncSlice);
if (!subSliceIsAInputLatencyAsyncSlice)
return;
// If |subSlice| does not have a typeName, then ignore it.
if (!subSlice.typeName)
return;
// If |this| already has a typeName and |subSlice| has a different
// typeName, then explode!
if (this.typeName_ && subSlice.typeName_) {
var subSliceHasDifferentTypeName = (
this.typeName_ !== subSlice.typeName_);
if (subSliceHasDifferentTypeName) {
throw 'InputLatencyAsyncSlice.determineLegacyTypeName_() ' +
' found multiple typeNames';
}
}
// The typeName of |this| top-level event is whatever the typeName of
// |subSlice| is. Set |this.typeName_| to the subSlice's typeName.
this.typeName_ = subSlice.typeName_;
}, this);
// If typeName could not be determined, then explode!
if (!this.typeName_)
throw 'InputLatencyAsyncSlice.determineLegacyTypeName_() failed';
// Check that the determined typeName is known.
this.checkTypeName_();
},
getRendererHelper: function(sourceSlices) {
var traceModel = this.startThread.parent.model;
if (!tr.e.audits.ChromeModelHelper.supportsModel(traceModel))
return undefined;
var mainThread = undefined;
var compositorThread = undefined;
for (var i in sourceSlices) {
if (sourceSlices[i].parentContainer.name ===
MAIN_RENDERER_THREAD_NAME)
mainThread = sourceSlices[i].parentContainer;
else if (sourceSlices[i].parentContainer.name ===
COMPOSITOR_THREAD_NAME)
compositorThread = sourceSlices[i].parentContainer;
if (mainThread && compositorThread)
break;
}
var modelHelper = new tr.e.audits.ChromeModelHelper(traceModel);
var rendererHelpers = modelHelper.rendererHelpers;
var pids = Object.keys(rendererHelpers);
for (var i = 0; i < pids.length; i++) {
var pid = pids[i];
var rendererHelper = rendererHelpers[pid];
if (rendererHelper.mainThread === mainThread ||
rendererHelper.compositorThread === compositorThread)
return rendererHelper;
}
return undefined;
},
addEntireSliceHierarchy: function(slice) {
this.associatedEvents_.push(slice);
slice.iterateAllSubsequentSlices(function(subsequentSlice) {
this.associatedEvents_.push(subsequentSlice);
}, this);
},
addDirectlyAssociatedEvents: function(flowEvents) {
var slices = [];
flowEvents.forEach(function(flowEvent) {
this.associatedEvents_.push(flowEvent);
var newSource = flowEvent.startSlice.mostTopLevelSlice;
if (slices.indexOf(newSource) === -1)
slices.push(newSource);
}, this);
var lastFlowEvent = flowEvents[flowEvents.length - 1];
var lastSource = lastFlowEvent.endSlice.mostTopLevelSlice;
if (slices.indexOf(lastSource) === -1)
slices.push(lastSource);
return slices;
},
// Find the Latency::ScrollUpdate slice that corresponds to the
// InputLatency::GestureScrollUpdate slice.
// The C++ CL that makes this connection is at:
// https://codereview.chromium.org/1178963003
addScrollUpdateEvents: function(rendererHelper) {
if (!rendererHelper || !rendererHelper.compositorThread)
return;
var compositorThread = rendererHelper.compositorThread;
var gestureScrollUpdateStart = this.start;
var gestureScrollUpdateEnd = this.end;
var allCompositorAsyncSlices =
compositorThread.asyncSliceGroup.slices;
for (var i in allCompositorAsyncSlices) {
var slice = allCompositorAsyncSlices[i];
if (slice.title !== 'Latency::ScrollUpdate')
continue;
var parentId = slice.args.data.
INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT.
sequence_number;
if (parentId === undefined) {
// Old trace, we can only rely on the timestamp to find the slice
if (slice.start < gestureScrollUpdateStart ||
slice.start >= gestureScrollUpdateEnd)
continue;
} else {
// New trace, we can definitively find the latency slice by comparing
// its sequence number with gesture id
if (parseInt(parentId) !== parseInt(this.id))
continue;
}
slice.associatedEvents.forEach(function(event) {
this.associatedEvents_.push(event);
}, this);
break;
}
},
// Return true if the slice hierarchy is tracked by LatencyInfo of other
// input latency events. If the slice hierarchy is tracked by both, this
// function still returns true.
belongToOtherInputs: function(slice, flowEvents) {
var fromOtherInputs = false;
slice.iterateEntireHierarchy(function(subsequentSlice) {
if (fromOtherInputs)
return;
subsequentSlice.inFlowEvents.forEach(function(inflow) {
if (fromOtherInputs)
return;
if (inflow.category.indexOf('input') > -1) {
if (flowEvents.indexOf(inflow) === -1)
fromOtherInputs = true;
}
}, this);
}, this);
return fromOtherInputs;
},
// Return true if |event| triggers slices of other inputs.
triggerOtherInputs: function(event, flowEvents) {
if (event.outFlowEvents === undefined ||
event.outFlowEvents.length === 0)
return false;
// Once we fix the bug of flow event binding, there should exist one and
// only one outgoing flow (PostTask) from ScheduleBeginImplFrameDeadline
// and PostComposite.
var flow = event.outFlowEvents[0];
if (flow.category !== POSTTASK_FLOW_EVENT ||
!flow.endSlice)
return false;
var endSlice = flow.endSlice;
if (this.belongToOtherInputs(endSlice.mostTopLevelSlice, flowEvents))
return true;
return false;
},
// Follow outgoing flow of subsequentSlices in the current hierarchy.
// We also handle cases where different inputs interfere with each other.
followSubsequentSlices: function(event, queue, visited, flowEvents) {
var stopFollowing = false;
var inputAck = false;
event.iterateAllSubsequentSlices(function(slice) {
if (stopFollowing)
return;
// Do not follow TaskQueueManager::RunTask because it causes
// many false events to be included.
if (slice.title === 'TaskQueueManager::RunTask')
return;
// Do not follow ScheduledActionSendBeginMainFrame because the real
// main thread BeginMainFrame is already traced by LatencyInfo flow.
if (slice.title === 'ThreadProxy::ScheduledActionSendBeginMainFrame')
return;
// Do not follow ScheduleBeginImplFrameDeadline that triggers an
// OnBeginImplFrameDeadline that is tracked by another LatencyInfo.
if (slice.title === 'Scheduler::ScheduleBeginImplFrameDeadline') {
if (this.triggerOtherInputs(slice, flowEvents))
return;
}
// Do not follow PostComposite that triggers CompositeImmediately
// that is tracked by another LatencyInfo.
if (slice.title === 'CompositorImpl::PostComposite') {
if (this.triggerOtherInputs(slice, flowEvents))
return;
}
// Stop following the rest of the current slice hierarchy if
// FilterAndSendWebInputEvent occurs after ProcessInputEventAck.
if (slice.title === 'InputRouterImpl::ProcessInputEventAck')
inputAck = true;
if (inputAck &&
slice.title === 'InputRouterImpl::FilterAndSendWebInputEvent')
stopFollowing = true;
this.followCurrentSlice(slice, queue, visited);
}, this);
},
// Follow outgoing flow events of the current slice.
followCurrentSlice: function(event, queue, visited) {
event.outFlowEvents.forEach(function(outflow) {
if ((outflow.category === POSTTASK_FLOW_EVENT ||
outflow.category === IPC_FLOW_EVENT) &&
outflow.endSlice) {
this.associatedEvents_.push(outflow);
var nextEvent = outflow.endSlice.mostTopLevelSlice;
if (!visited.contains(nextEvent)) {
visited.push(nextEvent);
queue.push(nextEvent);
}
}
}, this);
},
backtraceFromDraw: function(beginImplFrame, visited) {
var pendingEventQueue = [];
pendingEventQueue.push(beginImplFrame.mostTopLevelSlice);
while (pendingEventQueue.length !== 0) {
var event = pendingEventQueue.pop();
this.addEntireSliceHierarchy(event);
// TODO(yuhao): For now, we backtrace all the way to the source input.
// But is this really needed? I will have an entry in the design
// doc to discuss this.
event.inFlowEvents.forEach(function(inflow) {
if (inflow.category === POSTTASK_FLOW_EVENT && inflow.startSlice) {
var nextEvent = inflow.startSlice.mostTopLevelSlice;
if (!visited.contains(nextEvent)) {
visited.push(nextEvent);
pendingEventQueue.push(nextEvent);
}
}
}, this);
}
},
sortRasterizerSlices: function(rasterWorkerThreads,
sortedRasterizerSlices) {
rasterWorkerThreads.forEach(function(rasterizer) {
Array.prototype.push.apply(sortedRasterizerSlices,
rasterizer.sliceGroup.slices);
}, this);
sortedRasterizerSlices.sort(function(a, b) {
if (a.start !== b.start)
return a.start - b.start;
return a.guid - b.guid;
});
},
// Find rasterization slices that have the source_prepare_tiles_id
// same as the prepare_tiles_id of TileManager::PrepareTiles
// The C++ CL that makes this connection is at:
// https://codereview.chromium.org/1208683002/
addRasterizationEvents: function(prepareTiles, rendererHelper,
visited, flowEvents, sortedRasterizerSlices) {
if (!prepareTiles.args.prepare_tiles_id)
return;
if (!rendererHelper || !rendererHelper.rasterWorkerThreads)
return;
var rasterWorkerThreads = rendererHelper.rasterWorkerThreads;
var prepare_tile_id = prepareTiles.args.prepare_tiles_id;
var pendingEventQueue = [];
// Collect all the rasterizer tasks. Return the cached copy if possible.
if (sortedRasterizerSlices.length === 0)
this.sortRasterizerSlices(rasterWorkerThreads, sortedRasterizerSlices);
// TODO(yuhao): Once TaskSetFinishedTaskImpl also get the prepare_tile_id
// we can simply track by checking id rather than counting.
var numFinishedTasks = 0;
var RASTER_TASK_TITLE = 'RasterizerTaskImpl::RunOnWorkerThread';
var IMAGEDECODE_TASK_TITLE = 'ImageDecodeTaskImpl::RunOnWorkerThread';
var FINISHED_TASK_TITLE = 'TaskSetFinishedTaskImpl::RunOnWorkerThread';
for (var i = 0; i < sortedRasterizerSlices.length; i++) {
var task = sortedRasterizerSlices[i];
if (task.title === RASTER_TASK_TITLE ||
task.title === IMAGEDECODE_TASK_TITLE) {
if (task.args.source_prepare_tiles_id === prepare_tile_id)
this.addEntireSliceHierarchy(task.mostTopLevelSlice);
} else if (task.title === FINISHED_TASK_TITLE) {
if (task.start > prepareTiles.start) {
pendingEventQueue.push(task.mostTopLevelSlice);
if (++numFinishedTasks === 3)
break;
}
}
}
// Trace PostTask from rasterizer tasks.
while (pendingEventQueue.length != 0) {
var event = pendingEventQueue.pop();
this.addEntireSliceHierarchy(event);
this.followSubsequentSlices(event, pendingEventQueue, visited,
flowEvents);
}
},
addOtherCausallyRelatedEvents: function(rendererHelper, sourceSlices,
flowEvents, sortedRasterizerSlices) {
var pendingEventQueue = [];
// Keep track of visited nodes when traversing a DAG
var visitedEvents = new EventSet();
var beginImplFrame = undefined;
var prepareTiles = undefined;
var sortedRasterizerSlices = [];
sourceSlices.forEach(function(sourceSlice) {
if (!visitedEvents.contains(sourceSlice)) {
visitedEvents.push(sourceSlice);
pendingEventQueue.push(sourceSlice);
}
}, this);
while (pendingEventQueue.length != 0) {
var event = pendingEventQueue.pop();
// Push the current event chunk into associatedEvents.
this.addEntireSliceHierarchy(event);
this.followCurrentSlice(event, pendingEventQueue, visitedEvents);
this.followSubsequentSlices(event, pendingEventQueue, visitedEvents,
flowEvents);
// The rasterization work (CompositorTileWorker thread) and the
// Compositor tile manager are connect by the prepare_tiles_id
// instead of flow events.
var COMPOSITOR_PREPARE_TILES = 'TileManager::PrepareTiles';
prepareTiles = event.findDescendentSlice(COMPOSITOR_PREPARE_TILES);
if (prepareTiles)
this.addRasterizationEvents(prepareTiles, rendererHelper,
visitedEvents, flowEvents, sortedRasterizerSlices);
// OnBeginImplFrameDeadline could be triggered by other inputs.
// For now, we backtrace from it.
// TODO(yuhao): There are more such slices that we need to backtrace
var COMPOSITOR_ON_BIFD = 'Scheduler::OnBeginImplFrameDeadline';
beginImplFrame = event.findDescendentSlice(COMPOSITOR_ON_BIFD);
if (beginImplFrame)
this.backtraceFromDraw(beginImplFrame, visitedEvents);
}
// A separate pass on GestureScrollUpdate.
// Scroll update doesn't go through the main thread, but the compositor
// may go back to the main thread if there is an onscroll event handler.
// This is captured by a different flow event, which does not have the
// same ID as the Input Latency Event, but it is technically causally
// related to the GestureScrollUpdate input. Add them manually for now.
var INPUT_GSU = 'InputLatency::GestureScrollUpdate';
if (this.title === INPUT_GSU)
this.addScrollUpdateEvents(rendererHelper);
},
get associatedEvents() {
if (this.associatedEvents_.length !== 0)
return this.associatedEvents_;
var modelIndices = this.startThread.parent.model.modelIndices;
var flowEvents = modelIndices.getFlowEventsWithId(this.id);
if (flowEvents.length === 0)
return this.associatedEvents_;
// Step 1: Get events that are directly connected by the LatencyInfo
// flow events. This gives us a small set of events that are guaranteed
// to be associated with the input, but are almost certain incomplete.
// We call this set "source" event set.
// This step returns the "source" event set (sourceSlices), which is then
// used in the second step.
var sourceSlices = this.addDirectlyAssociatedEvents(flowEvents);
// Step 2: Start from the previously constructed "source" event set, we
// follow the toplevel (i.e., PostTask) and IPC flow events. Any slices
// that are reachable from the "source" event set via PostTasks or IPCs
// are conservatively considered associated with the input event.
// We then deal with specific cases where flow events either over include
// or miss capturing slices.
var rendererHelper = this.getRendererHelper(sourceSlices);
this.addOtherCausallyRelatedEvents(rendererHelper, sourceSlices,
flowEvents);
return this.associatedEvents_;
},
get inputLatency() {
if (!('data' in this.args))
return undefined;
var data = this.args.data;
if (!(END_COMP_NAME in data))
return undefined;
var latency = 0;
var endTime = data[END_COMP_NAME].time;
if (ORIGINAL_COMP_NAME in data) {
latency = endTime - data[ORIGINAL_COMP_NAME].time;
} else if (UI_COMP_NAME in data) {
latency = endTime - data[UI_COMP_NAME].time;
} else if (BEGIN_COMP_NAME in data) {
latency = endTime - data[BEGIN_COMP_NAME].time;
} else {
throw new Error('No valid begin latency component');
}
return latency;
}
};
var eventTypeNames = [
'Char',
'ContextMenu',
'GestureClick',
'GestureFlingCancel',
'GestureFlingStart',
'GestureScrollBegin',
'GestureScrollEnd',
'GestureScrollUpdate',
'GestureShowPress',
'GestureTap',
'GestureTapCancel',
'GestureTapDown',
'GesturePinchBegin',
'GesturePinchEnd',
'GesturePinchUpdate',
'KeyDown',
'KeyUp',
'MouseDown',
'MouseEnter',
'MouseLeave',
'MouseMove',
'MouseUp',
'MouseWheel',
'RawKeyDown',
'ScrollUpdate',
'TouchCancel',
'TouchEnd',
'TouchMove',
'TouchStart'
];
var allTypeNames = ['InputLatency'];
eventTypeNames.forEach(function(eventTypeName) {
// Old style.
allTypeNames.push('InputLatency:' + eventTypeName);
// New style.
allTypeNames.push('InputLatency::' + eventTypeName);
});
AsyncSlice.register(
InputLatencyAsyncSlice,
{
typeNames: allTypeNames,
categoryParts: ['latencyInfo']
});
return {
InputLatencyAsyncSlice: InputLatencyAsyncSlice,
INPUT_EVENT_TYPE_NAMES: INPUT_EVENT_TYPE_NAMES
};
});
</script>