text: // Copyright 2018 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.
  
  // @ts-nocheck
  // TODO(crbug.com/1011811): Enable TypeScript compiler checks
  
  import * as PerfUI from '../perf_ui/perf_ui.js';
  import * as Platform from '../platform/platform.js';
  import * as UI from '../ui/ui.js';
  
  /**
   * @unrestricted
   */
  export class HeapTimelineOverview extends UI.Widget.VBox {
    constructor() {
      super();
      this.element.id = 'heap-recording-view';
      this.element.classList.add('heap-tracking-overview');
  
      this._overviewCalculator = new OverviewCalculator();
      this._overviewContainer = this.element.createChild('div', 'heap-overview-container');
      this._overviewGrid = new PerfUI.OverviewGrid.OverviewGrid('heap-recording', this._overviewCalculator);
      this._overviewGrid.element.classList.add('fill');
  
      this._overviewCanvas = this._overviewContainer.createChild('canvas', 'heap-recording-overview-canvas');
      this._overviewContainer.appendChild(this._overviewGrid.element);
      this._overviewGrid.addEventListener(PerfUI.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
  
      this._windowLeft = 0.0;
      this._windowRight = 1.0;
      this._overviewGrid.setWindow(this._windowLeft, this._windowRight);
      this._yScale = new SmoothScale();
      this._xScale = new SmoothScale();
  
      this._profileSamples = new Samples();
    }
  
    start() {
      this._running = true;
      const drawFrame = () => {
        this.update();
        if (this._running) {
          this.element.window().requestAnimationFrame(drawFrame);
        }
      };
      drawFrame();
    }
  
    stop() {
      this._running = false;
    }
  
    /**
     * @param {!Samples} samples
     */
    setSamples(samples) {
      this._profileSamples = samples;
      if (!this._running) {
        this.update();
      }
    }
  
    /**
     * @param {number} width
     * @param {number} height
     */
    _drawOverviewCanvas(width, height) {
      if (!this._profileSamples) {
        return;
      }
      const profileSamples = this._profileSamples;
      const sizes = profileSamples.sizes;
      const topSizes = profileSamples.max;
      const timestamps = profileSamples.timestamps;
      const startTime = timestamps[0];
  
      const scaleFactor = this._xScale.nextScale(width / profileSamples.totalTime);
      let maxSize = 0;
      /**
       * @param {!Array.<number>} sizes
       * @param {function(number, number):void} callback
       */
      function aggregateAndCall(sizes, callback) {
        let size = 0;
        let currentX = 0;
        for (let i = 1; i < timestamps.length; ++i) {
          const x = Math.floor((timestamps[i] - startTime) * scaleFactor);
          if (x !== currentX) {
            if (size) {
              callback(currentX, size);
            }
            size = 0;
            currentX = x;
          }
          size += sizes[i];
        }
        callback(currentX, size);
      }
  
      /**
       * @param {number} x
       * @param {number} size
       */
      function maxSizeCallback(x, size) {
        maxSize = Math.max(maxSize, size);
      }
  
      aggregateAndCall(sizes, maxSizeCallback);
  
      const yScaleFactor = this._yScale.nextScale(maxSize ? height / (maxSize * 1.1) : 0.0);
  
      this._overviewCanvas.width = width * window.devicePixelRatio;
      this._overviewCanvas.height = height * window.devicePixelRatio;
      this._overviewCanvas.style.width = width + 'px';
      this._overviewCanvas.style.height = height + 'px';
  
      const context = this._overviewCanvas.getContext('2d');
      context.scale(window.devicePixelRatio, window.devicePixelRatio);
  
      if (this._running) {
        context.beginPath();
        context.lineWidth = 2;
        context.strokeStyle = 'rgba(192, 192, 192, 0.6)';
        const currentX = (Date.now() - startTime) * scaleFactor;
        context.moveTo(currentX, height - 1);
        context.lineTo(currentX, 0);
        context.stroke();
        context.closePath();
      }
  
      let gridY;
      let gridValue;
      const gridLabelHeight = 14;
      if (yScaleFactor) {
        const maxGridValue = (height - gridLabelHeight) / yScaleFactor;
        // The round value calculation is a bit tricky, because
        // it has a form k*10^n*1024^m, where k=[1,5], n=[0..3], m is an integer,
        // e.g. a round value 10KB is 10240 bytes.
        gridValue = Math.pow(1024, Math.floor(Math.log(maxGridValue) / Math.log(1024)));
        gridValue *= Math.pow(10, Math.floor(Math.log(maxGridValue / gridValue) / Math.LN10));
        if (gridValue * 5 <= maxGridValue) {
          gridValue *= 5;
        }
        gridY = Math.round(height - gridValue * yScaleFactor - 0.5) + 0.5;
        context.beginPath();
        context.lineWidth = 1;
        context.strokeStyle = 'rgba(0, 0, 0, 0.2)';
        context.moveTo(0, gridY);
        context.lineTo(width, gridY);
        context.stroke();
        context.closePath();
      }
  
      /**
       * @param {number} x
       * @param {number} size
       */
      function drawBarCallback(x, size) {
        context.moveTo(x, height - 1);
        context.lineTo(x, Math.round(height - size * yScaleFactor - 1));
      }
  
      context.beginPath();
      context.lineWidth = 2;
      context.strokeStyle = 'rgba(192, 192, 192, 0.6)';
      aggregateAndCall(topSizes, drawBarCallback);
      context.stroke();
      context.closePath();
  
      context.beginPath();
      context.lineWidth = 2;
      context.strokeStyle = 'rgba(0, 0, 192, 0.8)';
      aggregateAndCall(sizes, drawBarCallback);
      context.stroke();
      context.closePath();
  
      if (gridValue) {
        const label = Platform.NumberUtilities.bytesToString(gridValue);
        const labelPadding = 4;
        const labelX = 0;
        const labelY = gridY - 0.5;
        const labelWidth = 2 * labelPadding + context.measureText(label).width;
        context.beginPath();
        context.textBaseline = 'bottom';
        context.font = '10px ' + window.getComputedStyle(this.element, null).getPropertyValue('font-family');
        context.fillStyle = 'rgba(255, 255, 255, 0.75)';
        context.fillRect(labelX, labelY - gridLabelHeight, labelWidth, gridLabelHeight);
        context.fillStyle = 'rgb(64, 64, 64)';
        context.fillText(label, labelX + labelPadding, labelY);
        context.fill();
        context.closePath();
      }
    }
  
    /**
     * @override
     */
    onResize() {
      this._updateOverviewCanvas = true;
      this._scheduleUpdate();
    }
  
    _onWindowChanged() {
      if (!this._updateGridTimerId) {
        this._updateGridTimerId = setTimeout(this.updateGrid.bind(this), 10);
      }
    }
  
    _scheduleUpdate() {
      if (this._updateTimerId) {
        return;
      }
      this._updateTimerId = setTimeout(this.update.bind(this), 10);
    }
  
    _updateBoundaries() {
      this._windowLeft = this._overviewGrid.windowLeft();
      this._windowRight = this._overviewGrid.windowRight();
      this._windowWidth = this._windowRight - this._windowLeft;
    }
  
    update() {
      this._updateTimerId = null;
      if (!this.isShowing()) {
        return;
      }
      this._updateBoundaries();
      this._overviewCalculator._updateBoundaries(this);
      this._overviewGrid.updateDividers(this._overviewCalculator);
      this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - 20);
    }
  
    updateGrid() {
      this._updateGridTimerId = 0;
      this._updateBoundaries();
      const ids = this._profileSamples.ids;
      if (!ids.length) {
        return;
      }
      const timestamps = this._profileSamples.timestamps;
      const sizes = this._profileSamples.sizes;
      const startTime = timestamps[0];
      const totalTime = this._profileSamples.totalTime;
      const timeLeft = startTime + totalTime * this._windowLeft;
      const timeRight = startTime + totalTime * this._windowRight;
      const minIndex = timestamps.lowerBound(timeLeft);
      const maxIndex = timestamps.upperBound(timeRight);
      let size = 0;
      for (let i = minIndex; i <= maxIndex; ++i) {
        size += sizes[i];
      }
      const minId = minIndex > 0 ? ids[minIndex - 1] : 0;
      const maxId = maxIndex < ids.length ? ids[maxIndex] : Infinity;
  
      this.dispatchEventToListeners(IdsRangeChanged, {minId, maxId, size});
    }
  }
  
  export const IdsRangeChanged = Symbol('IdsRangeChanged');
  
  export class SmoothScale {
    constructor() {
      this._lastUpdate = 0;
      this._currentScale = 0.0;
    }
  
    /**
     * @param {number} target
     * @return {number}
     */
    nextScale(target) {
      target = target || this._currentScale;
      if (this._currentScale) {
        const now = Date.now();
        const timeDeltaMs = now - this._lastUpdate;
        this._lastUpdate = now;
        const maxChangePerSec = 20;
        const maxChangePerDelta = Math.pow(maxChangePerSec, timeDeltaMs / 1000);
        const scaleChange = target / this._currentScale;
        this._currentScale *= Platform.NumberUtilities.clamp(scaleChange, 1 / maxChangePerDelta, maxChangePerDelta);
      } else {
        this._currentScale = target;
      }
      return this._currentScale;
    }
  }
  
  /**
   * @unrestricted
   */
  export class Samples {
    constructor() {
      /** @type {!Array<number>} */
      this.sizes = [];
      /** @type {!Array<number>} */
      this.ids = [];
      /** @type {!Array<number>} */
      this.timestamps = [];
      /** @type {!Array<number>} */
      this.max = [];
      /** @type {number} */
      this.totalTime = 30000;
    }
  }
  
  /**
   * @implements {PerfUI.TimelineGrid.Calculator}
   * @unrestricted
   */
  export class OverviewCalculator {
    /**
     * @param {!HeapTimelineOverview} chart
     */
    _updateBoundaries(chart) {
      this._minimumBoundaries = 0;
      this._maximumBoundaries = chart._profileSamples.totalTime;
      this._xScaleFactor = chart._overviewContainer.clientWidth / this._maximumBoundaries;
    }
  
    /**
     * @override
     * @param {number} time
     * @return {number}
     */
    computePosition(time) {
      return (time - this._minimumBoundaries) * this._xScaleFactor;
    }
  
    /**
     * @override
     * @param {number} value
     * @param {number=} precision
     * @return {string}
     */
    formatValue(value, precision) {
      return Number.secondsToString(value / 1000, !!precision);
    }
  
    /**
     * @override
     * @return {number}
     */
    maximumBoundary() {
      return this._maximumBoundaries;
    }
  
    /**
     * @override
     * @return {number}
     */
    minimumBoundary() {
      return this._minimumBoundaries;
    }
  
    /**
     * @override
     * @return {number}
     */
    zeroTime() {
      return this._minimumBoundaries;
    }
  
    /**
     * @override
     * @return {number}
     */
    boundarySpan() {
      return this._maximumBoundaries - this._minimumBoundaries;
    }
  }
  