| // Copyright 2016 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. |
| |
| /** |
| * @unrestricted |
| */ |
| export class TimelineIRModel { |
| constructor() { |
| this.reset(); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {!Phases} |
| */ |
| static phaseForEvent(event) { |
| return event[TimelineIRModel._eventIRPhase]; |
| } |
| |
| /** |
| * @param {?Array<!SDK.TracingModel.AsyncEvent>} inputLatencies |
| * @param {?Array<!SDK.TracingModel.AsyncEvent>} animations |
| */ |
| populate(inputLatencies, animations) { |
| this.reset(); |
| if (!inputLatencies) { |
| return; |
| } |
| this._processInputLatencies(inputLatencies); |
| if (animations) { |
| this._processAnimations(animations); |
| } |
| const range = new Common.SegmentedRange(); |
| range.appendRange(this._drags); // Drags take lower precedence than animation, as we can't detect them reliably. |
| range.appendRange(this._cssAnimations); |
| range.appendRange(this._scrolls); |
| range.appendRange(this._responses); |
| this._segments = range.segments(); |
| } |
| |
| /** |
| * @param {!Array<!SDK.TracingModel.AsyncEvent>} events |
| */ |
| _processInputLatencies(events) { |
| const eventTypes = InputEvents; |
| const phases = Phases; |
| const thresholdsMs = TimelineIRModel._mergeThresholdsMs; |
| |
| let scrollStart; |
| let flingStart; |
| let touchStart; |
| let firstTouchMove; |
| let mouseWheel; |
| let mouseDown; |
| let mouseMove; |
| |
| for (let i = 0; i < events.length; ++i) { |
| const event = events[i]; |
| if (i > 0 && events[i].startTime < events[i - 1].startTime) { |
| console.assert(false, 'Unordered input events'); |
| } |
| const type = this._inputEventType(event.name); |
| switch (type) { |
| case eventTypes.ScrollBegin: |
| this._scrolls.append(this._segmentForEvent(event, phases.Scroll)); |
| scrollStart = event; |
| break; |
| |
| case eventTypes.ScrollEnd: |
| if (scrollStart) { |
| this._scrolls.append(this._segmentForEventRange(scrollStart, event, phases.Scroll)); |
| } else { |
| this._scrolls.append(this._segmentForEvent(event, phases.Scroll)); |
| } |
| scrollStart = null; |
| break; |
| |
| case eventTypes.ScrollUpdate: |
| touchStart = null; // Since we're scrolling now, disregard other touch gestures. |
| this._scrolls.append(this._segmentForEvent(event, phases.Scroll)); |
| break; |
| |
| case eventTypes.FlingStart: |
| if (flingStart) { |
| Common.console.error( |
| Common.UIString('Two flings at the same time? %s vs %s', flingStart.startTime, event.startTime)); |
| break; |
| } |
| flingStart = event; |
| break; |
| |
| case eventTypes.FlingCancel: |
| // FIXME: also process renderer fling events. |
| if (!flingStart) { |
| break; |
| } |
| this._scrolls.append(this._segmentForEventRange(flingStart, event, phases.Fling)); |
| flingStart = null; |
| break; |
| |
| case eventTypes.ImplSideFling: |
| this._scrolls.append(this._segmentForEvent(event, phases.Fling)); |
| break; |
| |
| case eventTypes.ShowPress: |
| case eventTypes.Tap: |
| case eventTypes.KeyDown: |
| case eventTypes.KeyDownRaw: |
| case eventTypes.KeyUp: |
| case eventTypes.Char: |
| case eventTypes.Click: |
| case eventTypes.ContextMenu: |
| this._responses.append(this._segmentForEvent(event, phases.Response)); |
| break; |
| |
| case eventTypes.TouchStart: |
| // We do not produce any response segment for TouchStart -- there's either going to be one upon |
| // TouchMove for drag, or one for GestureTap. |
| if (touchStart) { |
| Common.console.error( |
| Common.UIString('Two touches at the same time? %s vs %s', touchStart.startTime, event.startTime)); |
| break; |
| } |
| touchStart = event; |
| event.steps[0][TimelineIRModel._eventIRPhase] = phases.Response; |
| firstTouchMove = null; |
| break; |
| |
| case eventTypes.TouchCancel: |
| touchStart = null; |
| break; |
| |
| case eventTypes.TouchMove: |
| if (firstTouchMove) { |
| this._drags.append(this._segmentForEvent(event, phases.Drag)); |
| } else if (touchStart) { |
| firstTouchMove = event; |
| this._responses.append(this._segmentForEventRange(touchStart, event, phases.Response)); |
| } |
| break; |
| |
| case eventTypes.TouchEnd: |
| touchStart = null; |
| break; |
| |
| case eventTypes.MouseDown: |
| mouseDown = event; |
| mouseMove = null; |
| break; |
| |
| case eventTypes.MouseMove: |
| if (mouseDown && !mouseMove && mouseDown.startTime + thresholdsMs.mouse > event.startTime) { |
| this._responses.append(this._segmentForEvent(mouseDown, phases.Response)); |
| this._responses.append(this._segmentForEvent(event, phases.Response)); |
| } else if (mouseDown) { |
| this._drags.append(this._segmentForEvent(event, phases.Drag)); |
| } |
| mouseMove = event; |
| break; |
| |
| case eventTypes.MouseUp: |
| this._responses.append(this._segmentForEvent(event, phases.Response)); |
| mouseDown = null; |
| break; |
| |
| case eventTypes.MouseWheel: |
| // Do not consider first MouseWheel as trace viewer's implementation does -- in case of MouseWheel it's not really special. |
| if (mouseWheel && canMerge(thresholdsMs.mouse, mouseWheel, event)) { |
| this._scrolls.append(this._segmentForEventRange(mouseWheel, event, phases.Scroll)); |
| } else { |
| this._scrolls.append(this._segmentForEvent(event, phases.Scroll)); |
| } |
| mouseWheel = event; |
| break; |
| } |
| } |
| |
| /** |
| * @param {number} threshold |
| * @param {!SDK.TracingModel.AsyncEvent} first |
| * @param {!SDK.TracingModel.AsyncEvent} second |
| * @return {boolean} |
| */ |
| function canMerge(threshold, first, second) { |
| return first.endTime < second.startTime && second.startTime < first.endTime + threshold; |
| } |
| } |
| |
| /** |
| * @param {!Array<!SDK.TracingModel.AsyncEvent>} events |
| */ |
| _processAnimations(events) { |
| for (let i = 0; i < events.length; ++i) { |
| this._cssAnimations.append(this._segmentForEvent(events[i], Phases.Animation)); |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.AsyncEvent} event |
| * @param {!Phases} phase |
| * @return {!Common.Segment} |
| */ |
| _segmentForEvent(event, phase) { |
| this._setPhaseForEvent(event, phase); |
| return new Common.Segment(event.startTime, event.endTime, phase); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.AsyncEvent} startEvent |
| * @param {!SDK.TracingModel.AsyncEvent} endEvent |
| * @param {!Phases} phase |
| * @return {!Common.Segment} |
| */ |
| _segmentForEventRange(startEvent, endEvent, phase) { |
| this._setPhaseForEvent(startEvent, phase); |
| this._setPhaseForEvent(endEvent, phase); |
| return new Common.Segment(startEvent.startTime, endEvent.endTime, phase); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.AsyncEvent} asyncEvent |
| * @param {!Phases} phase |
| */ |
| _setPhaseForEvent(asyncEvent, phase) { |
| asyncEvent.steps[0][TimelineIRModel._eventIRPhase] = phase; |
| } |
| |
| /** |
| * @return {!Array<!Common.Segment>} |
| */ |
| interactionRecords() { |
| return this._segments; |
| } |
| |
| reset() { |
| const thresholdsMs = TimelineIRModel._mergeThresholdsMs; |
| |
| this._segments = []; |
| this._drags = new Common.SegmentedRange(merge.bind(null, thresholdsMs.mouse)); |
| this._cssAnimations = new Common.SegmentedRange(merge.bind(null, thresholdsMs.animation)); |
| this._responses = new Common.SegmentedRange(merge.bind(null, 0)); |
| this._scrolls = new Common.SegmentedRange(merge.bind(null, thresholdsMs.animation)); |
| |
| /** |
| * @param {number} threshold |
| * @param {!Common.Segment} first |
| * @param {!Common.Segment} second |
| */ |
| function merge(threshold, first, second) { |
| return first.end + threshold >= second.begin && first.data === second.data ? first : null; |
| } |
| } |
| |
| /** |
| * @param {string} eventName |
| * @return {?InputEvents} |
| */ |
| _inputEventType(eventName) { |
| const prefix = 'InputLatency::'; |
| if (!eventName.startsWith(prefix)) { |
| if (eventName === InputEvents.ImplSideFling) { |
| return /** @type {!InputEvents} */ (eventName); |
| } |
| console.error('Unrecognized input latency event: ' + eventName); |
| return null; |
| } |
| return /** @type {!InputEvents} */ (eventName.substr(prefix.length)); |
| } |
| } |
| |
| /** |
| * @enum {string} |
| */ |
| export const Phases = { |
| Idle: 'Idle', |
| Response: 'Response', |
| Scroll: 'Scroll', |
| Fling: 'Fling', |
| Drag: 'Drag', |
| Animation: 'Animation', |
| Uncategorized: 'Uncategorized' |
| }; |
| |
| /** |
| * @enum {string} |
| */ |
| export const InputEvents = { |
| Char: 'Char', |
| Click: 'GestureClick', |
| ContextMenu: 'ContextMenu', |
| FlingCancel: 'GestureFlingCancel', |
| FlingStart: 'GestureFlingStart', |
| ImplSideFling: TimelineModel.TimelineModel.RecordType.ImplSideFling, |
| KeyDown: 'KeyDown', |
| KeyDownRaw: 'RawKeyDown', |
| KeyUp: 'KeyUp', |
| LatencyScrollUpdate: 'ScrollUpdate', |
| MouseDown: 'MouseDown', |
| MouseMove: 'MouseMove', |
| MouseUp: 'MouseUp', |
| MouseWheel: 'MouseWheel', |
| PinchBegin: 'GesturePinchBegin', |
| PinchEnd: 'GesturePinchEnd', |
| PinchUpdate: 'GesturePinchUpdate', |
| ScrollBegin: 'GestureScrollBegin', |
| ScrollEnd: 'GestureScrollEnd', |
| ScrollUpdate: 'GestureScrollUpdate', |
| ScrollUpdateRenderer: 'ScrollUpdate', |
| ShowPress: 'GestureShowPress', |
| Tap: 'GestureTap', |
| TapCancel: 'GestureTapCancel', |
| TapDown: 'GestureTapDown', |
| TouchCancel: 'TouchCancel', |
| TouchEnd: 'TouchEnd', |
| TouchMove: 'TouchMove', |
| TouchStart: 'TouchStart' |
| }; |
| |
| TimelineIRModel._mergeThresholdsMs = { |
| animation: 1, |
| mouse: 40, |
| }; |
| |
| TimelineIRModel._eventIRPhase = Symbol('eventIRPhase'); |
| |
| /* Legacy exported object */ |
| self.TimelineModel = self.TimelineModel || {}; |
| |
| /* Legacy exported object */ |
| TimelineModel = TimelineModel || {}; |
| |
| /** @constructor */ |
| TimelineModel.TimelineIRModel = TimelineIRModel; |
| |
| /** @enum {string} */ |
| TimelineModel.TimelineIRModel.Phases = Phases; |
| |
| /** @enum {string} */ |
| TimelineModel.TimelineIRModel.InputEvents = InputEvents; |