UNPKG

chrome-devtools-frontend

Version:
368 lines (328 loc) • 11.7 kB
// 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. import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as SDK from '../sdk/sdk.js'; // eslint-disable-line no-unused-vars import {RecordType} from './TimelineModel.js'; export const UIStrings = { /** *@description Text in Timeline IRModel of the Performance panel *@example {2s} PH1 *@example {3s} PH2 */ twoFlingsAtTheSameTimeSVsS: 'Two flings at the same time? {PH1} vs {PH2}', /** *@description Text in Timeline IRModel of the Performance panel *@example {2s} PH1 *@example {3s} PH2 */ twoTouchesAtTheSameTimeSVsS: 'Two touches at the same time? {PH1} vs {PH2}', }; const str_ = i18n.i18n.registerUIStrings('timeline_model/TimelineIRModel.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** * @type {!WeakMap<!SDK.TracingModel.Event, !Phases>} */ const eventToPhase = new WeakMap(); export class TimelineIRModel { constructor() { // Attributes below are guaranteed to be set by reset(); /** @type {!Array<!Common.SegmentedRange.Segment>} */ this._segments; /** @type {!Common.SegmentedRange.SegmentedRange} */ this._drags; /** @type {!Common.SegmentedRange.SegmentedRange} */ this._cssAnimations; /** @type {!Common.SegmentedRange.SegmentedRange} */ this._responses; /** @type {!Common.SegmentedRange.SegmentedRange} */ this._scrolls; this.reset(); } /** * @param {!SDK.TracingModel.Event} event * @return {(!Phases|undefined)} */ static phaseForEvent(event) { return eventToPhase.get(event); } /** * @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.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.Console.instance().error( i18nString(UIStrings.twoFlingsAtTheSameTimeSVsS, {PH1: flingStart.startTime, PH2: 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.Console.instance().error( i18nString(UIStrings.twoTouchesAtTheSameTimeSVsS, {PH1: touchStart.startTime, PH2: event.startTime})); break; } touchStart = event; this._setPhaseForEvent(event, 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) { if (first.endTime === undefined) { return false; } 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.SegmentedRange.Segment} */ _segmentForEvent(event, phase) { this._setPhaseForEvent(event, phase); return new Common.SegmentedRange.Segment( event.startTime, event.endTime !== undefined ? event.endTime : Number.MAX_SAFE_INTEGER, phase); } /** * @param {!SDK.TracingModel.AsyncEvent} startEvent * @param {!SDK.TracingModel.AsyncEvent} endEvent * @param {!Phases} phase * @return {!Common.SegmentedRange.Segment} */ _segmentForEventRange(startEvent, endEvent, phase) { this._setPhaseForEvent(startEvent, phase); this._setPhaseForEvent(endEvent, phase); return new Common.SegmentedRange.Segment( startEvent.startTime, startEvent.endTime !== undefined ? startEvent.endTime : Number.MAX_SAFE_INTEGER, phase); } /** * @param {!SDK.TracingModel.AsyncEvent} asyncEvent * @param {!Phases} phase */ _setPhaseForEvent(asyncEvent, phase) { eventToPhase.set(asyncEvent.steps[0], phase); } /** * @return {!Array<!Common.SegmentedRange.Segment>} */ interactionRecords() { return this._segments; } reset() { const thresholdsMs = TimelineIRModel._mergeThresholdsMs; this._segments = []; this._drags = new Common.SegmentedRange.SegmentedRange(merge.bind(null, thresholdsMs.mouse)); this._cssAnimations = new Common.SegmentedRange.SegmentedRange(merge.bind(null, thresholdsMs.animation)); this._responses = new Common.SegmentedRange.SegmentedRange(merge.bind(null, 0)); this._scrolls = new Common.SegmentedRange.SegmentedRange(merge.bind(null, thresholdsMs.animation)); /** * @param {number} threshold * @param {!Common.SegmentedRange.Segment} first * @param {!Common.SegmentedRange.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: 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');