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

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

There is a newer version: 10.6.27
Show newest version
/**
 * Copyright (C) 2014 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import * as Common from '../common/common.js';
import * as Host from '../host/host.js';
import * as PerfUI from '../perf_ui/perf_ui.js';
import * as UI from '../ui/ui.js';

/**
 * @implements {PerfUI.FlameChart.FlameChartDataProvider}
 * @unrestricted
 */
export class ProfileFlameChartDataProvider {
  constructor() {
    this._colorGenerator = ProfileFlameChartDataProvider.colorGenerator();
  }

  /**
   * @return {!Common.Color.Generator}
   */
  static colorGenerator() {
    if (!ProfileFlameChartDataProvider._colorGenerator) {
      const colorGenerator =
          new Common.Color.Generator({min: 30, max: 330}, {min: 50, max: 80, count: 5}, {min: 80, max: 90, count: 3});

      colorGenerator.setColorForID('(idle)', 'hsl(0, 0%, 94%)');
      colorGenerator.setColorForID('(program)', 'hsl(0, 0%, 80%)');
      colorGenerator.setColorForID('(garbage collector)', 'hsl(0, 0%, 80%)');
      ProfileFlameChartDataProvider._colorGenerator = colorGenerator;
    }
    return ProfileFlameChartDataProvider._colorGenerator;
  }

  /**
   * @override
   * @return {number}
   */
  minimumBoundary() {
    return this._cpuProfile.profileStartTime;
  }

  /**
   * @override
   * @return {number}
   */
  totalTime() {
    return this._cpuProfile.profileHead.total;
  }

  /**
   * @override
   * @param {number} value
   * @param {number=} precision
   * @return {string}
   */
  formatValue(value, precision) {
    return Number.preciseMillisToString(value, precision);
  }

  /**
   * @override
   * @return {number}
   */
  maxStackDepth() {
    return this._maxStackDepth;
  }

  /**
   * @override
   * @return {?PerfUI.FlameChart.TimelineData}
   */
  timelineData() {
    return this._timelineData || this._calculateTimelineData();
  }

  /**
   * @return {!PerfUI.FlameChart.TimelineData}
   */
  _calculateTimelineData() {
    throw 'Not implemented.';
  }

  /**
   * @override
   * @param {number} entryIndex
   * @return {?Element}
   */
  prepareHighlightedEntryInfo(entryIndex) {
    throw 'Not implemented.';
  }

  /**
   * @override
   * @param {number} entryIndex
   * @return {boolean}
   */
  canJumpToEntry(entryIndex) {
    return this._entryNodes[entryIndex].scriptId !== '0';
  }

  /**
   * @override
   * @param {number} entryIndex
   * @return {string}
   */
  entryTitle(entryIndex) {
    const node = this._entryNodes[entryIndex];
    return UI.UIUtils.beautifyFunctionName(node.functionName);
  }

  /**
   * @override
   * @param {number} entryIndex
   * @return {?string}
   */
  entryFont(entryIndex) {
    if (!this._font) {
      this._font = '11px ' + Host.Platform.fontFamily();
      this._boldFont = 'bold ' + this._font;
    }
    const node = this._entryNodes[entryIndex];
    return node.deoptReason ? this._boldFont : this._font;
  }

  /**
   * @override
   * @param {number} entryIndex
   * @return {string}
   */
  entryColor(entryIndex) {
    const node = this._entryNodes[entryIndex];
    // For idle and program, we want different 'shades of gray', so we fallback to functionName as scriptId = 0
    // For rest of nodes e.g eval scripts, if url is empty then scriptId will be guaranteed to be non-zero
    return this._colorGenerator.colorForID(node.url || (node.scriptId !== '0' ? node.scriptId : node.functionName));
  }

  /**
   * @override
   * @param {number} entryIndex
   * @param {!CanvasRenderingContext2D} context
   * @param {?string} text
   * @param {number} barX
   * @param {number} barY
   * @param {number} barWidth
   * @param {number} barHeight
   * @return {boolean}
   */
  decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight) {
    return false;
  }

  /**
   * @override
   * @param {number} entryIndex
   * @return {boolean}
   */
  forceDecoration(entryIndex) {
    return false;
  }

  /**
   * @override
   * @param {number} entryIndex
   * @return {string}
   */
  textColor(entryIndex) {
    return '#333';
  }
}


/**
 * @implements {UI.SearchableView.Searchable}
 * @unrestricted
 */
export class CPUProfileFlameChart extends UI.Widget.VBox {
  /**
   * @param {!UI.SearchableView.SearchableView} searchableView
   * @param {!ProfileFlameChartDataProvider} dataProvider
   */
  constructor(searchableView, dataProvider) {
    super();
    this.element.id = 'cpu-flame-chart';

    this._searchableView = searchableView;
    this._overviewPane = new OverviewPane(dataProvider);
    this._overviewPane.show(this.element);

    this._mainPane = new PerfUI.FlameChart.FlameChart(dataProvider, this._overviewPane);
    this._mainPane.setBarHeight(15);
    this._mainPane.setTextBaseline(4);
    this._mainPane.setTextPadding(2);
    this._mainPane.show(this.element);
    this._mainPane.addEventListener(PerfUI.FlameChart.Events.EntrySelected, this._onEntrySelected, this);
    this._mainPane.addEventListener(PerfUI.FlameChart.Events.EntryInvoked, this._onEntryInvoked, this);
    this._entrySelected = false;
    this._mainPane.addEventListener(PerfUI.FlameChart.Events.CanvasFocused, this._onEntrySelected, this);
    this._overviewPane.addEventListener(PerfUI.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
    this._dataProvider = dataProvider;
    this._searchResults = [];
  }

  /**
   * @override
   */
  focus() {
    this._mainPane.focus();
  }

  /**
   * @param {!Common.EventTarget.EventTargetEvent} event
   */
  _onWindowChanged(event) {
    const windowLeft = event.data.windowTimeLeft;
    const windowRight = event.data.windowTimeRight;
    this._mainPane.setWindowTimes(windowLeft, windowRight, /* animate */ true);
  }

  /**
   * @param {number} timeLeft
   * @param {number} timeRight
   */
  selectRange(timeLeft, timeRight) {
    this._overviewPane._selectRange(timeLeft, timeRight);
  }

  /**
   * @param {!Common.EventTarget.EventTargetEvent} event
   */
  _onEntrySelected(event) {
    if (event.data) {
      const eventIndex = Number(event.data);
      this._mainPane.setSelectedEntry(eventIndex);
      if (eventIndex === -1) {
        this._entrySelected = false;
      } else {
        this._entrySelected = true;
      }
    } else if (!this._entrySelected) {
      this._mainPane.setSelectedEntry(0);
      this._entrySelected = true;
    }
  }

  /**
   * @param {!Common.EventTarget.EventTargetEvent} event
   */
  _onEntryInvoked(event) {
    this._onEntrySelected(event);
    this.dispatchEventToListeners(PerfUI.FlameChart.Events.EntryInvoked, event.data);
  }

  update() {
    this._overviewPane.update();
    this._mainPane.update();
  }

  /**
   * @override
   * @param {!UI.SearchableView.SearchConfig} searchConfig
   * @param {boolean} shouldJump
   * @param {boolean=} jumpBackwards
   */
  performSearch(searchConfig, shouldJump, jumpBackwards) {
    const matcher = createPlainTextSearchRegex(searchConfig.query, searchConfig.caseSensitive ? '' : 'i');

    const selectedEntryIndex = this._searchResultIndex !== -1 ? this._searchResults[this._searchResultIndex] : -1;
    this._searchResults = [];
    const entriesCount = this._dataProvider._entryNodes.length;
    for (let index = 0; index < entriesCount; ++index) {
      if (this._dataProvider.entryTitle(index).match(matcher)) {
        this._searchResults.push(index);
      }
    }

    if (this._searchResults.length) {
      this._searchResultIndex = this._searchResults.indexOf(selectedEntryIndex);
      if (this._searchResultIndex === -1) {
        this._searchResultIndex = jumpBackwards ? this._searchResults.length - 1 : 0;
      }
      this._mainPane.setSelectedEntry(this._searchResults[this._searchResultIndex]);
    } else {
      this.searchCanceled();
    }
    this._searchableView.updateSearchMatchesCount(this._searchResults.length);
    this._searchableView.updateCurrentMatchIndex(this._searchResultIndex);
  }

  /**
   * @override
   */
  searchCanceled() {
    this._mainPane.setSelectedEntry(-1);
    this._searchResults = [];
    this._searchResultIndex = -1;
  }

  /**
   * @override
   */
  jumpToNextSearchResult() {
    this._searchResultIndex = (this._searchResultIndex + 1) % this._searchResults.length;
    this._mainPane.setSelectedEntry(this._searchResults[this._searchResultIndex]);
    this._searchableView.updateCurrentMatchIndex(this._searchResultIndex);
  }

  /**
   * @override
   */
  jumpToPreviousSearchResult() {
    this._searchResultIndex = (this._searchResultIndex - 1 + this._searchResults.length) % this._searchResults.length;
    this._mainPane.setSelectedEntry(this._searchResults[this._searchResultIndex]);
    this._searchableView.updateCurrentMatchIndex(this._searchResultIndex);
  }

  /**
   * @override
   * @return {boolean}
   */
  supportsCaseSensitiveSearch() {
    return true;
  }

  /**
   * @override
   * @return {boolean}
   */
  supportsRegexSearch() {
    return false;
  }
}

/**
 * @implements {PerfUI.TimelineGrid.Calculator}
 * @unrestricted
 */
export class OverviewCalculator {
  constructor(dataProvider) {
    this._dataProvider = dataProvider;
  }

  /**
   * @param {!OverviewPane} overviewPane
   */
  _updateBoundaries(overviewPane) {
    this._minimumBoundaries = overviewPane._dataProvider.minimumBoundary();
    const totalTime = overviewPane._dataProvider.totalTime();
    this._maximumBoundaries = this._minimumBoundaries + totalTime;
    this._xScaleFactor = overviewPane._overviewContainer.clientWidth / totalTime;
  }

  /**
   * @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 this._dataProvider.formatValue(value - this._minimumBoundaries, 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;
  }
}

/**
 * @implements {PerfUI.FlameChart.FlameChartDelegate}
 * @unrestricted
 */
export class OverviewPane extends UI.Widget.VBox {
  /**
   * @param {!PerfUI.FlameChart.FlameChartDataProvider} dataProvider
   */
  constructor(dataProvider) {
    super();
    this.element.classList.add('cpu-profile-flame-chart-overview-pane');
    this._overviewContainer = this.element.createChild('div', 'cpu-profile-flame-chart-overview-container');
    this._overviewCalculator = new OverviewCalculator(dataProvider);
    this._overviewGrid = new PerfUI.OverviewGrid.OverviewGrid('cpu-profile-flame-chart', this._overviewCalculator);
    this._overviewGrid.element.classList.add('fill');
    this._overviewCanvas = this._overviewContainer.createChild('canvas', 'cpu-profile-flame-chart-overview-canvas');
    this._overviewContainer.appendChild(this._overviewGrid.element);
    this._dataProvider = dataProvider;
    this._overviewGrid.addEventListener(PerfUI.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
  }

  /**
   * @override
   * @param {number} windowStartTime
   * @param {number} windowEndTime
   */
  windowChanged(windowStartTime, windowEndTime) {
    this._selectRange(windowStartTime, windowEndTime);
  }

  /**
   * @override
   * @param {number} startTime
   * @param {number} endTime
   */
  updateRangeSelection(startTime, endTime) {
  }

  /**
   * @override
   * @param {!PerfUI.FlameChart.FlameChart} flameChart
   * @param {?PerfUI.FlameChart.Group} group
   */
  updateSelectedGroup(flameChart, group) {
  }

  /**
   * @param {number} timeLeft
   * @param {number} timeRight
   */
  _selectRange(timeLeft, timeRight) {
    const startTime = this._dataProvider.minimumBoundary();
    const totalTime = this._dataProvider.totalTime();
    this._overviewGrid.setWindow((timeLeft - startTime) / totalTime, (timeRight - startTime) / totalTime);
  }

  /**
   * @param {!Common.EventTarget.EventTargetEvent} event
   */
  _onWindowChanged(event) {
    const windowPosition = {windowTimeLeft: event.data.rawStartValue, windowTimeRight: event.data.rawEndValue};
    this._windowTimeLeft = windowPosition.windowTimeLeft;
    this._windowTimeRight = windowPosition.windowTimeRight;

    this.dispatchEventToListeners(PerfUI.OverviewGrid.Events.WindowChanged, windowPosition);
  }

  /**
   * @return {?PerfUI.FlameChart.TimelineData}
   */
  _timelineData() {
    return this._dataProvider.timelineData();
  }

  /**
   * @override
   */
  onResize() {
    this._scheduleUpdate();
  }

  _scheduleUpdate() {
    if (this._updateTimerId) {
      return;
    }
    this._updateTimerId = this.element.window().requestAnimationFrame(this.update.bind(this));
  }

  update() {
    this._updateTimerId = 0;
    const timelineData = this._timelineData();
    if (!timelineData) {
      return;
    }
    this._resetCanvas(
        this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - PerfUI.FlameChart.HeaderHeight);
    this._overviewCalculator._updateBoundaries(this);
    this._overviewGrid.updateDividers(this._overviewCalculator);
    this._drawOverviewCanvas();
  }

  _drawOverviewCanvas() {
    const canvasWidth = this._overviewCanvas.width;
    const canvasHeight = this._overviewCanvas.height;
    const drawData = this._calculateDrawData(canvasWidth);
    const context = this._overviewCanvas.getContext('2d');
    const ratio = window.devicePixelRatio;
    const offsetFromBottom = ratio;
    const lineWidth = 1;
    const yScaleFactor = canvasHeight / (this._dataProvider.maxStackDepth() * 1.1);
    context.lineWidth = lineWidth;
    context.translate(0.5, 0.5);
    context.strokeStyle = 'rgba(20,0,0,0.4)';
    context.fillStyle = 'rgba(214,225,254,0.8)';
    context.moveTo(-lineWidth, canvasHeight + lineWidth);
    context.lineTo(-lineWidth, Math.round(canvasHeight - drawData[0] * yScaleFactor - offsetFromBottom));
    let value;
    for (let x = 0; x < canvasWidth; ++x) {
      value = Math.round(canvasHeight - drawData[x] * yScaleFactor - offsetFromBottom);
      context.lineTo(x, value);
    }
    context.lineTo(canvasWidth + lineWidth, value);
    context.lineTo(canvasWidth + lineWidth, canvasHeight + lineWidth);
    context.fill();
    context.stroke();
    context.closePath();
  }

  /**
   * @param {number} width
   * @return {!Uint8Array}
   */
  _calculateDrawData(width) {
    const dataProvider = this._dataProvider;
    const timelineData = this._timelineData();
    const entryStartTimes = timelineData.entryStartTimes;
    const entryTotalTimes = timelineData.entryTotalTimes;
    const entryLevels = timelineData.entryLevels;
    const length = entryStartTimes.length;
    const minimumBoundary = this._dataProvider.minimumBoundary();

    const drawData = new Uint8Array(width);
    const scaleFactor = width / dataProvider.totalTime();

    for (let entryIndex = 0; entryIndex < length; ++entryIndex) {
      const start = Math.floor((entryStartTimes[entryIndex] - minimumBoundary) * scaleFactor);
      const finish =
          Math.floor((entryStartTimes[entryIndex] - minimumBoundary + entryTotalTimes[entryIndex]) * scaleFactor);
      for (let x = start; x <= finish; ++x) {
        drawData[x] = Math.max(drawData[x], entryLevels[entryIndex] + 1);
      }
    }
    return drawData;
  }

  /**
   * @param {number} width
   * @param {number} height
   */
  _resetCanvas(width, height) {
    const ratio = window.devicePixelRatio;
    this._overviewCanvas.width = width * ratio;
    this._overviewCanvas.height = height * ratio;
    this._overviewCanvas.style.width = width + 'px';
    this._overviewCanvas.style.height = height + 'px';
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy