text: // 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
   */
  TimelineModel.TimelineIRModel = class {
    constructor() {
      this.reset();
    }
  
    /**
     * @param {!SDK.TracingModel.Event} event
     * @return {!TimelineModel.TimelineIRModel.Phases}
     */
    static phaseForEvent(event) {
      return event[TimelineModel.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);
      var 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) {
      var eventTypes = TimelineModel.TimelineIRModel.InputEvents;
      var phases = TimelineModel.TimelineIRModel.Phases;
      var thresholdsMs = TimelineModel.TimelineIRModel._mergeThresholdsMs;
  
      var scrollStart;
      var flingStart;
      var touchStart;
      var firstTouchMove;
      var mouseWheel;
      var mouseDown;
      var mouseMove;
  
      for (var i = 0; i < events.length; ++i) {
        var event = events[i];
        if (i > 0 && events[i].startTime < events[i - 1].startTime)
          console.assert(false, 'Unordered input events');
        var 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][TimelineModel.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 (var i = 0; i < events.length; ++i)
        this._cssAnimations.append(this._segmentForEvent(events[i], TimelineModel.TimelineIRModel.Phases.Animation));
    }
  
    /**
     * @param {!SDK.TracingModel.AsyncEvent} event
     * @param {!TimelineModel.TimelineIRModel.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 {!TimelineModel.TimelineIRModel.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 {!TimelineModel.TimelineIRModel.Phases} phase
     */
    _setPhaseForEvent(asyncEvent, phase) {
      asyncEvent.steps[0][TimelineModel.TimelineIRModel._eventIRPhase] = phase;
    }
  
    /**
     * @return {!Array<!Common.Segment>}
     */
    interactionRecords() {
      return this._segments;
    }
  
    reset() {
      var thresholdsMs = TimelineModel.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 {?TimelineModel.TimelineIRModel.InputEvents}
     */
    _inputEventType(eventName) {
      var prefix = 'InputLatency::';
      if (!eventName.startsWith(prefix)) {
        if (eventName === TimelineModel.TimelineIRModel.InputEvents.ImplSideFling)
          return /** @type {!TimelineModel.TimelineIRModel.InputEvents} */ (eventName);
        console.error('Unrecognized input latency event: ' + eventName);
        return null;
      }
      return /** @type {!TimelineModel.TimelineIRModel.InputEvents} */ (eventName.substr(prefix.length));
    }
  };
  
  /**
   * @enum {string}
   */
  TimelineModel.TimelineIRModel.Phases = {
    Idle: 'Idle',
    Response: 'Response',
    Scroll: 'Scroll',
    Fling: 'Fling',
    Drag: 'Drag',
    Animation: 'Animation',
    Uncategorized: 'Uncategorized'
  };
  
  /**
   * @enum {string}
   */
  TimelineModel.TimelineIRModel.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'
  };
  
  TimelineModel.TimelineIRModel._mergeThresholdsMs = {
    animation: 1,
    mouse: 40,
  };
  
  TimelineModel.TimelineIRModel._eventIRPhase = Symbol('eventIRPhase');
  