| <!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/base.html"> |
| <link rel="import" href="/tracing/base/range_utils.html"> |
| <link rel="import" href="/tracing/core/auditor.html"> |
| <link rel="import" href="/tracing/extras/chrome/cc/input_latency_async_slice.html"> |
| <link rel="import" href="/tracing/extras/chrome/chrome_model_helper.html"> |
| <link rel="import" href="/tracing/extras/rail/idle_interaction_record.html"> |
| <link rel="import" href="/tracing/extras/rail/load_interaction_record.html"> |
| <link rel="import" href="/tracing/extras/rail/proto_ir.html"> |
| <link rel="import" href="/tracing/model/event_info.html"> |
| <link rel="import" href="/tracing/model/ir_coverage.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; |
| } |
| |
| function causedFrame(event) { |
| for (var i = 0; i < event.associatedEvents.length; ++i) { |
| if (event.associatedEvents[i].title === tr.e.audits.IMPL_RENDERING_STATS) |
| return true; |
| } |
| return false; |
| } |
| |
| function forEventTypesIn(events, typeNames, cb, opt_this) { |
| events.forEach(function(event) { |
| if (typeNames.indexOf(event.typeName) >= 0) { |
| cb.call(opt_this, event); |
| } |
| }); |
| } |
| |
| var RENDER_FRAME_IMPL_PREFIX = 'RenderFrameImpl::'; |
| var CREATE_CHILD_TITLE = RENDER_FRAME_IMPL_PREFIX + 'createChildFrame'; |
| var START_LOAD_TITLE = RENDER_FRAME_IMPL_PREFIX + 'didStartProvisionalLoad'; |
| var FAIL_LOAD_TITLE = RENDER_FRAME_IMPL_PREFIX + 'didFailProvisionalLoad'; |
| var FINISH_LOAD_TITLE = RENDER_FRAME_IMPL_PREFIX + 'didFinishLoad'; |
| |
| // This is an instant event that is a subSlice of a FINISH_LOAD_TITLE |
| // event. |
| var LOAD_FINISHED_TITLE = 'LoadFinished'; |
| |
| function isRenderFrameImplEvent(event) { |
| return event.title.indexOf(RENDER_FRAME_IMPL_PREFIX) === 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; |
| |
| // 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; |
| |
| 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.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 CSS_ANIMATION_TITLE = 'Animation'; |
| |
| // Strings used to name IRs. |
| var LOAD_STARTUP_IR_NAME = 'Startup'; |
| var LOAD_SUCCEEDED_IR_NAME = 'Succeeded'; |
| var LOAD_FAILED_IR_NAME = 'Failed'; |
| 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'; |
| |
| 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)); |
| this.collectUnassociatedEvents_(rirs); |
| return rirs; |
| }, |
| |
| setIRNames_: function(name, irs) { |
| irs.forEach(function(ir) { |
| ir.name = name; |
| }); |
| }, |
| |
| // Find all unassociated top-level ThreadSlices. If they start during an |
| // Idle or Load IR, then add their entire hierarchy to that IR. |
| collectUnassociatedEvents_: function(rirs) { |
| var vacuumIRs = []; |
| rirs.forEach(function(ir) { |
| if (ir instanceof tr.e.rail.LoadInteractionRecord || |
| ir instanceof tr.e.rail.IdleInteractionRecord) |
| vacuumIRs.push(ir); |
| }); |
| if (vacuumIRs.length === 0) |
| return; |
| |
| var allAssociatedEvents = tr.model.getAssociatedEvents(rirs); |
| var unassociatedEvents = tr.model.getUnassociatedEvents( |
| this.model, allAssociatedEvents); |
| |
| unassociatedEvents.forEach(function(event) { |
| if (!(event instanceof tr.model.ThreadSlice)) |
| return; |
| |
| if (!event.isTopLevel) |
| return; |
| |
| for (var iri = 0; iri < vacuumIRs.length; ++iri) { |
| var ir = vacuumIRs[iri]; |
| |
| if ((event.start >= ir.start) && |
| (event.start < ir.end)) { |
| ir.associatedEvents.addEventSet(event.entireHierarchy); |
| return; |
| } |
| } |
| }); |
| }, |
| |
| // 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 = []; |
| var model = this.model; |
| emptyRanges.forEach(function(range) { |
| // Ignore insignificantly tiny idle ranges. |
| if (range.max < (range.min + INSIGNIFICANT_MS)) |
| return; |
| irs.push(new tr.e.rail.IdleInteractionRecord( |
| model, 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 === START_LOAD_TITLE; |
| } |
| return this.modelHelper.browserHelper.getAllAsyncSlicesMatching( |
| isStartLoadSlice).sort(compareEvents); |
| }, |
| |
| getFailLoadEvents: function() { |
| function isFailLoadSlice(slice) { |
| return slice.title === FAIL_LOAD_TITLE; |
| } |
| return this.modelHelper.browserHelper.getAllAsyncSlicesMatching( |
| isFailLoadSlice).sort(compareEvents); |
| }, |
| |
| // If a thread contains a typical initialization slice, then the first event |
| // on that thread is a startup event. |
| getStartupEvents: function() { |
| function isStartupSlice(slice) { |
| return slice.title === 'BrowserMainLoop::CreateThreads'; |
| } |
| var events = this.modelHelper.browserHelper.getAllAsyncSlicesMatching( |
| isStartupSlice); |
| var deduper = new tr.model.EventSet(); |
| events.forEach(function(event) { |
| var sliceGroup = event.parentContainer.sliceGroup; |
| var slice = sliceGroup && sliceGroup.findFirstSlice(); |
| if (slice) |
| deduper.push(slice); |
| }); |
| return deduper.toArray(); |
| }, |
| |
| // Match every event in |openingEvents| to the first following event from |
| // |closingEvents| and return an array containing a load interaction record |
| // for each pair. |
| findLoadInteractionRecords_: function(openingEvents, closingEvents) { |
| var lirs = []; |
| var model = this.model; |
| openingEvents.forEach(function(openingEvent) { |
| closingEvents.forEach(function(closingEvent) { |
| // Ignore opening event that already have a closing event. |
| if (openingEvent.closingEvent) |
| return; |
| |
| // Ignore closing events that already belong to an opening event. |
| if (closingEvent.openingEvent) |
| return; |
| |
| // Ignore closing events before |openingEvent|. |
| if (closingEvent.start <= openingEvent.start) |
| return; |
| |
| // Ignore events from different threads. |
| if (openingEvent.parentContainer.parent.pid !== |
| closingEvent.parentContainer.parent.pid) |
| return; |
| |
| // This is the first closing event for this opening event, record it. |
| openingEvent.closingEvent = closingEvent; |
| closingEvent.openingEvent = openingEvent; |
| var lir = new tr.e.rail.LoadInteractionRecord( |
| model, openingEvent.start, |
| closingEvent.end - openingEvent.start); |
| lir.associatedEvents.push(openingEvent); |
| lir.associatedEvents.push(closingEvent); |
| |
| // All RenderFrameImpl events contain the routingId. |
| // |openingEvent| may be either didStartProvisionaLoad or |
| // didCommitProvisionalLoad, so use a general prefix test. |
| if (isRenderFrameImplEvent(openingEvent)) { |
| var renderProcessId = openingEvent.parentContainer.parent.pid; |
| lir.renderProcess = this.model.processes[renderProcessId]; |
| lir.renderMainThread = lir.renderProcess.findAtMostOneThreadNamed( |
| 'CrRendererMain'); |
| lir.routingId = openingEvent.args.id; |
| lir.parentRoutingId = this.findLoadParentRoutingId_(lir); |
| this.findLoadFinishedEvent_(lir); |
| } |
| lirs.push(lir); |
| }, this); |
| }, this); |
| return lirs; |
| }, |
| |
| // Find the routingId of the createChildFrame event that created the Load |
| // IR's RenderFrame. |
| findLoadParentRoutingId_: function(lir) { |
| var createChildEvent = undefined; |
| lir.renderMainThread.iterateAllEvents(function(event) { |
| if (event.title !== CREATE_CHILD_TITLE) |
| return; |
| |
| if (event.args.child !== lir.routingId) |
| return; |
| |
| createChildEvent = event; |
| }); |
| |
| if (!createChildEvent) |
| return undefined; |
| |
| return createChildEvent.args.id; |
| }, |
| |
| findLoadFinishedEvent_: function(lir) { |
| // First, find the RenderFrameImpl::didFinishLoad event that indicates a |
| // successful load. |
| |
| var finishLoadEvent = undefined; |
| lir.renderMainThread.iterateAllEvents(function(event) { |
| if (event.title !== FINISH_LOAD_TITLE) |
| return; |
| |
| if (event.start < lir.start) |
| return; |
| |
| // TODO(benjhayden) This part of the heuristic is problematic for now |
| // because |lir.end| is naively the first paint after the load starts. |
| if (event.start > lir.end) |
| return; |
| |
| if (event.args.id !== lir.routingId) |
| return; |
| |
| finishLoadEvent = event; |
| }); |
| |
| if (!finishLoadEvent) |
| return undefined; |
| |
| lir.associatedEvents.push(finishLoadEvent); |
| |
| // Then, see if finishLoadEvent contains a subSlice titled |
| // 'LoadFinished', which indicates that the load was for a main frame. |
| |
| var loadFinishedEvent = undefined; |
| finishLoadEvent.subSlices.forEach(function(event) { |
| if (event.title !== LOAD_FINISHED_TITLE) |
| return; |
| |
| loadFinishedEvent = event; |
| }); |
| |
| if (!loadFinishedEvent) |
| return; |
| |
| lir.loadFinishedEvent = loadFinishedEvent; |
| lir.associatedEvents.push(loadFinishedEvent); |
| }, |
| |
| // Match up RenderFrameImpl events with frame render events. |
| findLoadInteractionRecords: function() { |
| var startupEvents = this.getStartupEvents(); |
| var commitLoadEvents = |
| this.modelHelper.browserHelper.getCommitProvisionalLoadEventsInRange( |
| this.model.bounds); |
| var frameEvents = this.getAllFrameEvents(); |
| var startLoadEvents = this.getStartLoadEvents(); |
| var failLoadEvents = this.getFailLoadEvents(); |
| var lirs = []; |
| |
| // Attach frame events to every startup events. |
| var startupLIRs = this.findLoadInteractionRecords_(startupEvents, |
| frameEvents); |
| this.setIRNames_(LOAD_STARTUP_IR_NAME, startupLIRs); |
| lirs.push.apply(lirs, startupLIRs); |
| |
| // Attach frame events to every commit load events. |
| var successfulLIRs = this.findLoadInteractionRecords_(commitLoadEvents, |
| frameEvents); |
| this.setIRNames_(LOAD_SUCCEEDED_IR_NAME, successfulLIRs); |
| successfulLIRs.forEach(function(lir) { |
| // If a successful Load IR has a loadFinishedEvent, then it is a main |
| // frame. |
| // Drop sub-frame Loads for now. |
| if (lir.loadFinishedEvent) |
| lirs.push(lir); |
| }); |
| |
| // Attach fail load events to every start load events. |
| var failedLIRs = this.findLoadInteractionRecords_(startLoadEvents, |
| failLoadEvents); |
| this.setIRNames_(LOAD_FAILED_IR_NAME, failedLIRs); |
| failedLIRs.forEach(function(lir) { |
| // If a failed Load IR has a parentRoutingId, then it is a sub-frame. |
| // Drop sub-frame Loads for now. |
| if (lir.parentRoutingId === undefined) |
| lirs.push(lir); |
| }); |
| |
| return lirs; |
| }, |
| |
| // Find ProtoIRs, post-process them, convert them to real IRs. |
| findInputInteractionRecords: function() { |
| var sortedInputEvents = this.getSortedInputEvents(); |
| var protoIRs = this.findProtoIRs(sortedInputEvents); |
| protoIRs = this.postProcessProtoIRs(protoIRs); |
| this.checkAllInputEventsHandled(sortedInputEvents, protoIRs); |
| |
| var irs = []; |
| var model = this.model; |
| protoIRs.forEach(function(protoIR) { |
| var ir = protoIR.createInteractionRecord(model); |
| if (ir) |
| irs.push(ir); |
| }); |
| return irs; |
| }, |
| |
| findProtoIRs: function(sortedInputEvents) { |
| var protoIRs = []; |
| // This order is not important. Handlers are independent. |
| var handlers = [ |
| this.handleKeyboardEvents, |
| this.handleMouseResponseEvents, |
| this.handleMouseWheelEvents, |
| this.handleMouseDragEvents, |
| this.handleTapResponseEvents, |
| this.handlePinchEvents, |
| this.handleFlingEvents, |
| this.handleTouchEvents, |
| this.handleScrollEvents, |
| this.handleCSSAnimations |
| ]; |
| handlers.forEach(function(handler) { |
| protoIRs.push.apply(protoIRs, handler.call(this, sortedInputEvents)); |
| }, this); |
| protoIRs.sort(compareEvents); |
| return protoIRs; |
| }, |
| |
| getSortedInputEvents: function() { |
| var inputEvents = []; |
| |
| var browserProcess = this.modelHelper.browserHelper.process; |
| var mainThread = browserProcess.findAtMostOneThreadNamed( |
| 'CrBrowserMain'); |
| mainThread.asyncSliceGroup.iterateAllEvents(function(slice) { |
| if (!slice.isTopLevel) |
| return; |
| |
| if (!(slice instanceof tr.e.cc.InputLatencyAsyncSlice)) |
| return; |
| |
| // 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)) |
| return; |
| |
| inputEvents.push(slice); |
| }, this); |
| |
| return inputEvents.sort(compareEvents); |
| }, |
| |
| // Every keyboard event is a Response. |
| handleKeyboardEvents: function(sortedInputEvents) { |
| var protoIRs = []; |
| forEventTypesIn(sortedInputEvents, KEYBOARD_TYPE_NAMES, function(event) { |
| var pir = new ProtoIR(ProtoIR.RESPONSE_TYPE, KEYBOARD_IR_NAME); |
| pir.pushEvent(event); |
| protoIRs.push(pir); |
| }); |
| return protoIRs; |
| }, |
| |
| // Some mouse events can be translated directly into Responses. |
| handleMouseResponseEvents: function(sortedInputEvents) { |
| var protoIRs = []; |
| forEventTypesIn( |
| sortedInputEvents, MOUSE_RESPONSE_TYPE_NAMES, function(event) { |
| var pir = new ProtoIR(ProtoIR.RESPONSE_TYPE, MOUSE_IR_NAME); |
| pir.pushEvent(event); |
| protoIRs.push(pir); |
| }); |
| return protoIRs; |
| }, |
| |
| // 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 |
| // ProtoIR.isNear, which compares the end time of the previous event with |
| // the start time of the next. |
| handleMouseWheelEvents: function(sortedInputEvents) { |
| var protoIRs = []; |
| var currentPIR = 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 (currentPIR && |
| (prevEvent.start + MOUSE_WHEEL_THRESHOLD_MS) >= event.start) { |
| if (currentPIR.irType === ProtoIR.ANIMATION_TYPE) { |
| currentPIR.pushEvent(event); |
| } else { |
| currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE, |
| MOUSEWHEEL_IR_NAME); |
| currentPIR.pushEvent(event); |
| protoIRs.push(currentPIR); |
| } |
| return; |
| } |
| currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE, MOUSEWHEEL_IR_NAME); |
| currentPIR.pushEvent(event); |
| protoIRs.push(currentPIR); |
| }); |
| 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(sortedInputEvents) { |
| var protoIRs = []; |
| var currentPIR = undefined; |
| var mouseDownEvent = undefined; |
| forEventTypesIn( |
| sortedInputEvents, MOUSE_DRAG_TYPE_NAMES, function(event) { |
| switch (event.typeName) { |
| case INPUT_TYPE.MOUSE_DOWN: |
| if (causedFrame(event)) { |
| var pir = new ProtoIR(ProtoIR.RESPONSE_TYPE, MOUSE_IR_NAME); |
| pir.pushEvent(event); |
| protoIRs.push(pir); |
| } 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 pir = new ProtoIR(ProtoIR.IGNORED_TYPE); |
| pir.pushEvent(event); |
| protoIRs.push(pir); |
| } else if (!currentPIR || |
| !currentPIR.isNear(event, MOUSE_MOVE_THRESHOLD_MS)) { |
| // The first MouseMove after a MouseDown or after a while is a |
| // Response. |
| currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE, MOUSE_IR_NAME); |
| currentPIR.pushEvent(event); |
| if (mouseDownEvent) { |
| currentPIR.associatedEvents.push(mouseDownEvent); |
| mouseDownEvent = undefined; |
| } |
| protoIRs.push(currentPIR); |
| } else { |
| // Merge this event into an Animation. |
| if (currentPIR.irType === ProtoIR.ANIMATION_TYPE) { |
| currentPIR.pushEvent(event); |
| } else { |
| currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE, MOUSE_IR_NAME); |
| currentPIR.pushEvent(event); |
| protoIRs.push(currentPIR); |
| } |
| } |
| break; |
| |
| case INPUT_TYPE.MOUSE_UP: |
| if (!mouseDownEvent) { |
| var pir = new ProtoIR(causedFrame(event) ? ProtoIR.RESPONSE_TYPE : |
| ProtoIR.IGNORED_TYPE, MOUSE_IR_NAME); |
| pir.pushEvent(event); |
| protoIRs.push(pir); |
| break; |
| } |
| |
| if (currentPIR) { |
| currentPIR.pushEvent(event); |
| } else { |
| currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE, MOUSE_IR_NAME); |
| if (mouseDownEvent) |
| currentPIR.associatedEvents.push(mouseDownEvent); |
| currentPIR.pushEvent(event); |
| protoIRs.push(currentPIR); |
| } |
| mouseDownEvent = undefined; |
| 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(sortedInputEvents) { |
| var protoIRs = []; |
| var currentPIR = undefined; |
| forEventTypesIn(sortedInputEvents, TAP_TYPE_NAMES, function(event) { |
| switch (event.typeName) { |
| case INPUT_TYPE.TAP_DOWN: |
| currentPIR = new ProtoIR(ProtoIR.RESPONSE_TYPE, TAP_IR_NAME); |
| 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, TAP_IR_NAME); |
| 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, TAP_IR_NAME); |
| 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(sortedInputEvents) { |
| var protoIRs = []; |
| var currentPIR = undefined; |
| var sawFirstUpdate = false; |
| var modelBounds = this.model.bounds; |
| forEventTypesIn(sortedInputEvents, PINCH_TYPE_NAMES, 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, PINCH_IR_NAME); |
| 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, PINCH_IR_NAME); |
| 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(sortedInputEvents) { |
| var protoIRs = []; |
| var currentPIR = undefined; |
| |
| function isRendererFling(event) { |
| return event.title === RENDERER_FLING_TITLE; |
| } |
| var browserHelper = this.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 (currentPIR) { |
| currentPIR.pushEvent(event); |
| } else { |
| currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE, FLING_IR_NAME); |
| 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, FLING_IR_NAME); |
| 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(sortedInputEvents) { |
| var protoIRs = []; |
| var currentPIR = undefined; |
| var sawFirstMove = false; |
| forEventTypesIn(sortedInputEvents, TOUCH_TYPE_NAMES, 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, TOUCH_IR_NAME); |
| currentPIR.pushEvent(event); |
| protoIRs.push(currentPIR); |
| sawFirstMove = false; |
| } |
| break; |
| |
| case INPUT_TYPE.TOUCH_MOVE: |
| if (!currentPIR) { |
| currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE, TOUCH_IR_NAME); |
| 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, TOUCH_IR_NAME); |
| 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(sortedInputEvents) { |
| var protoIRs = []; |
| var currentPIR = undefined; |
| var sawFirstUpdate = false; |
| forEventTypesIn(sortedInputEvents, SCROLL_TYPE_NAMES, 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, SCROLL_IR_NAME); |
| 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, |
| SCROLL_IR_NAME); |
| currentPIR.pushEvent(event); |
| protoIRs.push(currentPIR); |
| } |
| } else { |
| // ScrollUpdate without ScrollBegin. |
| currentPIR = new ProtoIR(ProtoIR.ANIMATION_TYPE, SCROLL_IR_NAME); |
| 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(sortedInputEvents) { |
| var animationEvents = this.modelHelper.browserHelper. |
| getAllAsyncSlicesMatching(function(event) { |
| return ((event.title === CSS_ANIMATION_TITLE) && |
| (event.duration > 0)); |
| }); |
| |
| 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, CSS_IR_NAME); |
| 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); |
| protoIRs = this.fixTapResponseTouchAnimations(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; |
| |
| var isCSS = pir.containsSliceTitle(CSS_ANIMATION_TITLE); |
| var isFling = pir.containsTypeNames([INPUT_TYPE.FLING_START]); |
| |
| for (var i = 0; i < protoIRs.length; ++i) { |
| var otherPIR = protoIRs[i]; |
| |
| if (otherPIR.irType !== pir.irType) |
| continue; |
| |
| // Don't merge CSS Animations with any other types. |
| if (isCSS != otherPIR.containsSliceTitle(CSS_ANIMATION_TITLE)) |
| continue; |
| |
| if (!otherPIR.intersects(pir)) |
| continue; |
| |
| // Don't merge Fling Animations with any other types. |
| if (isFling != otherPIR.containsTypeNames([INPUT_TYPE.FLING_START])) |
| 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; |
| }, |
| |
| // Merge Tap Responses that overlap Touch-only Animations. |
| // https://github.com/catapult-project/catapult/issues/1431 |
| fixTapResponseTouchAnimations: function(protoIRs) { |
| function isTapResponse(pir) { |
| return (pir.irType === ProtoIR.RESPONSE_TYPE) && |
| pir.containsTypeNames([INPUT_TYPE.TAP]); |
| } |
| function isTouchAnimation(pir) { |
| return (pir.irType === ProtoIR.ANIMATION_TYPE) && |
| pir.containsTypeNames([INPUT_TYPE.TOUCH_MOVE]) && |
| !pir.containsTypeNames([ |
| INPUT_TYPE.SCROLL_UPDATE, INPUT_TYPE.PINCH_UPDATE]); |
| } |
| var newPIRs = []; |
| while (protoIRs.length) { |
| var pir = protoIRs.shift(); |
| newPIRs.push(pir); |
| |
| // protoIRs are sorted by start time, and we don't know whether the Tap |
| // Response or the Touch Animation will be first |
| var pirIsTapResponse = isTapResponse(pir); |
| var pirIsTouchAnimation = isTouchAnimation(pir); |
| if (!pirIsTapResponse && !pirIsTouchAnimation) |
| continue; |
| |
| for (var i = 0; i < protoIRs.length; ++i) { |
| var otherPIR = protoIRs[i]; |
| |
| if (!otherPIR.intersects(pir)) |
| continue; |
| |
| if (pirIsTapResponse && !isTouchAnimation(otherPIR)) |
| continue; |
| |
| if (pirIsTouchAnimation && !isTapResponse(otherPIR)) |
| continue; |
| |
| // pir might be the Touch Animation, but the merged ProtoIR should be |
| // a Response. |
| pir.irType = ProtoIR.RESPONSE_TYPE; |
| |
| pir.merge(otherPIR); |
| protoIRs.splice(i, 1); |
| // Don't skip the next otherPIR! |
| --i; |
| } |
| } |
| return newPIRs; |
| }, |
| |
| // Check that none of the handlers accidentally ignored an input event. |
| checkAllInputEventsHandled: function(sortedInputEvents, 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); |
| }); |
| }); |
| |
| sortedInputEvents.forEach(function(event) { |
| if (handledEvents.indexOf(event) < 0) { |
| console.error('UNHANDLED INPUT EVENT!', |
| event.typeName, parseInt(event.start), parseInt(event.end)); |
| } |
| }); |
| } |
| }; |
| |
| function createCustomizeModelLinesFromModel(model) { |
| var modelLines = []; |
| modelLines.push(' audits.addEvent(model.browserMain,'); |
| modelLines.push(' {title: \'model start\', start: 0, end: 1});'); |
| |
| var typeNames = {}; |
| for (var typeName in tr.e.cc.INPUT_EVENT_TYPE_NAMES) { |
| typeNames[tr.e.cc.INPUT_EVENT_TYPE_NAMES[typeName]] = typeName; |
| } |
| |
| var modelEvents = new tr.model.EventSet(); |
| model.interactionRecords.forEach(function(ir, index) { |
| modelEvents.addEventSet(ir.sourceEvents); |
| }); |
| modelEvents = modelEvents.toArray(); |
| modelEvents.sort(compareEvents); |
| |
| modelEvents.forEach(function(event) { |
| var startAndEnd = 'start: ' + parseInt(event.start) + ', ' + |
| 'end: ' + parseInt(event.end) + '});'; |
| if (event instanceof tr.e.cc.InputLatencyAsyncSlice) { |
| modelLines.push(' audits.addInputEvent(model, INPUT_TYPE.' + |
| typeNames[event.typeName] + ','); |
| } else if (event.title === 'RenderFrameImpl::didCommitProvisionalLoad') { |
| modelLines.push(' audits.addCommitLoadEvent(model,'); |
| } else if (event.title === |
| 'InputHandlerProxy::HandleGestureFling::started') { |
| modelLines.push(' audits.addFlingAnimationEvent(model,'); |
| } else if (event.title === tr.e.audits.IMPL_RENDERING_STATS) { |
| modelLines.push(' audits.addFrameEvent(model,'); |
| } else if (event.title === CSS_ANIMATION_TITLE) { |
| modelLines.push(' audits.addEvent(model.rendererMain, {'); |
| modelLines.push(' title: \'Animation\', ' + startAndEnd); |
| return; |
| } else { |
| throw ('You must extend createCustomizeModelLinesFromModel()' + |
| 'to support this event:\n' + event.title + '\n'); |
| } |
| modelLines.push(' {' + startAndEnd); |
| }); |
| |
| modelLines.push(' audits.addEvent(model.browserMain,'); |
| modelLines.push(' {' + |
| 'title: \'model end\', ' + |
| 'start: ' + (parseInt(model.bounds.max) - 1) + ', ' + |
| 'end: ' + parseInt(model.bounds.max) + '});'); |
| return modelLines; |
| } |
| |
| function createExpectedIRLinesFromModel(model) { |
| var expectedLines = []; |
| var irCount = model.interactionRecords.length; |
| model.interactionRecords.forEach(function(ir, index) { |
| var irString = ' {'; |
| irString += 'title: \'' + ir.title + '\', '; |
| irString += 'start: ' + parseInt(ir.start) + ', '; |
| irString += 'end: ' + parseInt(ir.end) + ', '; |
| irString += 'eventCount: ' + ir.sourceEvents.length; |
| irString += '}'; |
| if (index < (irCount - 1)) |
| irString += ','; |
| expectedLines.push(irString); |
| }); |
| return expectedLines; |
| } |
| |
| function createIRFinderTestCaseStringFromModel(model) { |
| var filename = window.location.hash.substr(1); |
| var testName = filename.substr(filename.lastIndexOf('/') + 1); |
| testName = testName.substr(0, testName.indexOf('.')); |
| |
| // createCustomizeModelLinesFromModel() throws an error if there's an |
| // unsupported event. |
| try { |
| var testLines = []; |
| testLines.push(' /*'); |
| testLines.push(' This test was generated from'); |
| testLines.push(' ' + filename + ''); |
| testLines.push(' */'); |
| testLines.push(' test(\'' + testName + '\', function() {'); |
| testLines.push(' var verifier = new IRVerifier();'); |
| testLines.push(' verifier.customizeModelCallback = function(model) {'); |
| testLines.push.apply(testLines, |
| createCustomizeModelLinesFromModel(model)); |
| testLines.push(' };'); |
| testLines.push(' verifier.expectedIRs = ['); |
| testLines.push.apply(testLines, createExpectedIRLinesFromModel(model)); |
| testLines.push(' ];'); |
| testLines.push(' verifier.verify();'); |
| testLines.push(' });'); |
| return testLines.join('\n'); |
| } catch (error) { |
| return error; |
| } |
| } |
| |
| return { |
| RAILIRFinder: RAILIRFinder, |
| createIRFinderTestCaseStringFromModel: createIRFinderTestCaseStringFromModel |
| }; |
| }); |
| </script> |