All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.dev-tools.profiler.HeapTimelineOverview.js Maven / Gradle / Ivy

// 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.

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.} 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 = Number.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} */
    this.sizes = [];
    /** @type {!Array} */
    this.ids = [];
    /** @type {!Array} */
    this.timestamps = [];
    /** @type {!Array} */
    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;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy