<commit_before>/*
 * Copyright (C) 2013 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.
 */

/**
 * @constructor
 * @extends {WebInspector.View}
 * @param {WebInspector.FlameChartDataProvider} dataProvider
 */
WebInspector.FlameChart = function(dataProvider)
{
    WebInspector.View.call(this);
    this.registerRequiredCSS("flameChart.css");
    this.element.className = "fill";
    this.element.id = "cpu-flame-chart";

    this._overviewContainer = this.element.createChild("div", "overview-container");
    this._overviewGrid = new WebInspector.OverviewGrid("flame-chart");
    this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas");
    this._overviewContainer.appendChild(this._overviewGrid.element);
    this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator();
    this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);

    this._chartContainer = this.element.createChild("div", "chart-container");
    this._timelineGrid = new WebInspector.TimelineGrid();
    this._chartContainer.appendChild(this._timelineGrid.element);
    this._calculator = new WebInspector.FlameChart.Calculator();

    this._canvas = this._chartContainer.createChild("canvas");
    this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this));
    WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize");

    this._entryInfo = this._chartContainer.createChild("div", "entry-info");

    this._dataProvider = dataProvider;
    this._windowLeft = 0.0;
    this._windowRight = 1.0;
    this._barHeight = 15;
    this._minWidth = 1;
    this._paddingLeft = 15;
    this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
    this._canvas.addEventListener("click", this._onClick.bind(this), false);
    this._linkifier = new WebInspector.Linkifier();
    this._highlightedEntryIndex = -1;

    if (!WebInspector.FlameChart._colorGenerator)
        WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator();
}

/**
 * @constructor
 */
WebInspector.FlameChartDataProvider = function(cpuProfileView)
{
    this._cpuProfileView = cpuProfileView;
}

WebInspector.FlameChartDataProvider.prototype = {
    /**
     * @param {WebInspector.FlameChart.ColorGenerator} colorGenerator
     * @return {Object}
     */
    timelineData: function(colorGenerator)
    {
        return this._timelineData || this._calculateTimelineData(colorGenerator);
    },

    /**
     * @param {WebInspector.FlameChart.ColorGenerator} colorGenerator
     * @return {Object}
     */
    _calculateTimelineData: function(colorGenerator)
    {
        if (!this._cpuProfileView.profileHead)
            return null;

        var samples = this._cpuProfileView.samples;
        var idToNode = this._cpuProfileView._idToNode;
        var gcNode = this._cpuProfileView._gcNode;
        var samplesCount = samples.length;
        var samplingInterval = this._cpuProfileView.samplingIntervalMs;

        var index = 0;
        var entries = /** @type {Array.<!WebInspector.FlameChart.Entry>} */ ([]);

        var openIntervals = [];
        var stackTrace = [];
        var colorEntryIndexes = [];
        var maxDepth = 5; // minimum stack depth for the case when we see no activity.
        var depth = 0;

        for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) {
            var node = idToNode[samples[sampleIndex]];
            stackTrace.length = 0;
            while (node) {
                stackTrace.push(node);
                node = node.parent;
            }
            stackTrace.pop(); // Remove (root) node

            maxDepth = Math.max(maxDepth, depth);
            depth = 0;
            node = stackTrace.pop();
            var intervalIndex;

            // GC samples have no stack, so we just put GC node on top of the last recoreded sample.
            if (node === gcNode) {
                while (depth < openIntervals.length) {
                    intervalIndex = openIntervals[depth].index;
                    entries[intervalIndex].duration += samplingInterval;
                    ++depth;
                }
                // If previous stack is also GC then just continue.
                if (openIntervals.length > 0 && openIntervals.peekLast().node === node) {
                    entries[intervalIndex].selfTime += samplingInterval;
                    continue;
                }
            }

            while (node && depth < openIntervals.length && node === openIntervals[depth].node) {
                intervalIndex = openIntervals[depth].index;
                entries[intervalIndex].duration += samplingInterval;
                node = stackTrace.pop();
                ++depth;
            }
            if (depth < openIntervals.length)
                openIntervals.length = depth;
            if (!node) {
                entries[intervalIndex].selfTime += samplingInterval;
                continue;
            }

            while (node) {
                var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
                var indexesForColor = colorEntryIndexes[colorPair.index];
                if (!indexesForColor)
                    indexesForColor = colorEntryIndexes[colorPair.index] = [];

                var entry = new WebInspector.FlameChart.Entry(colorPair, depth, samplingInterval, sampleIndex * samplingInterval, node);
                indexesForColor.push(entries.length);
                entries.push(entry);
                openIntervals.push({node: node, index: index});
                ++index;

                node = stackTrace.pop();
                ++depth;
            }
            entries[entries.length - 1].selfTime += samplingInterval;
        }

        var entryColorIndexes = new Uint16Array(entries.length);
        var entryLevels = new Uint8Array(entries.length);
        var entryTotalTimes = new Float32Array(entries.length);
        var entryOffsets = new Float32Array(entries.length);
        var entryTitles = new Array(entries.length);
        var entryDeoptFlags = new Uint8Array(entries.length);

        for (var i = 0; i < entries.length; ++i) {
            var entry = entries[i];
            entryColorIndexes[i] = colorPair.index;
            entryLevels[i] = entry.depth;
            entryTotalTimes[i] = entry.duration;
            entryOffsets[i] = entry.startTime;
            entryTitles[i] = entry.node.functionName;
            var reason = entry.node.deoptReason;
            entryDeoptFlags[i] = (reason && reason !== "no reason");
        }

        this._timelineData = {
            maxStackDepth: Math.max(maxDepth, depth),
            entries: entries,
            totalTime: this._cpuProfileView.profileHead.totalTime,
            entryColorIndexes: entryColorIndexes,
            entryLevels: entryLevels,
            entryTotalTimes: entryTotalTimes,
            entryOffsets: entryOffsets,
            colorEntryIndexes: colorEntryIndexes,
            entryTitles: entryTitles,
            entryDeoptFlags: entryDeoptFlags
        };

        return this._timelineData;
    },

    /**
     * @param {number} ms
     */
    _millisecondsToString: function(ms)
    {
        if (ms === 0)
            return "0";
        if (ms < 1000)
            return WebInspector.UIString("%.1f\u2009ms", ms);
        return Number.secondsToString(ms / 1000, true);
    },

    /**
     * @param {number} entryIndex
     */
    prepareHighlightedEntryInfo: function(entryIndex)
    {
        var entry = this._timelineData.entries[entryIndex];
        if (!entry)
            return null;
        var node = entry.node;
        if (!node)
            return null;

        var entryInfo = [];
        function pushEntryInfoRow(title, text)
        {
            var row = {};
            row.title = title;
            row.text = text;
            entryInfo.push(row);
        }

        pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName);
        var selfTime = this._millisecondsToString(entry.selfTime);
        var totalTime = this._millisecondsToString(entry.duration);
        pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime);
        pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime);
        if (node.url)
            pushEntryInfoRow(WebInspector.UIString("URL"), node.url + ":" + node.lineNumber);
        pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true));
        pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true));
        if (node.deoptReason && node.deoptReason !== "no reason")
            pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason);

        return entryInfo;
    },

    /**
     * @param {number} entryIndex
     * @return {boolean}
     */
    canJumpToEntry: function(entryIndex)
    {
        return this._timelineData.entries[entryIndex].node.scriptId !== "0";
    },

    /**
     * @param {number} entryIndex
     * @return {Object}
     */
    entryData: function(entryIndex)
    {
        return this._timelineData.entries[entryIndex].node;
    }
}

/**
 * @constructor
 * @implements {WebInspector.TimelineGrid.Calculator}
 */
WebInspector.FlameChart.Calculator = function()
{
}

WebInspector.FlameChart.Calculator.prototype = {
    /**
     * @param {WebInspector.FlameChart} flameChart
     */
    _updateBoundaries: function(flameChart)
    {
        function log10(x)
        {
            return Math.log(x) / Math.LN10;
        }
        this._decimalDigits = Math.max(0, -Math.floor(log10(flameChart._timelineGrid.gridSliceTime * 1.01)));
        var totalTime = flameChart._timelineData().totalTime;
        this._minimumBoundaries = flameChart._windowLeft * totalTime;
        this._maximumBoundaries = flameChart._windowRight * totalTime;
        this.paddingLeft = flameChart._paddingLeft;
        this._width = flameChart._canvas.width - this.paddingLeft;
        this._timeToPixel = this._width / this.boundarySpan();
    },

    /**
     * @param {number} time
     * @return {number}
     */
    computePosition: function(time)
    {
        return (time - this._minimumBoundaries) * this._timeToPixel + this.paddingLeft;
    },

    /**
     * @param {number} value
     * @return {string}
     */
    formatTime: function(value)
    {
        var format = "%." + this._decimalDigits + "f\u2009ms";
        return WebInspector.UIString(format, value + this._minimumBoundaries);
    },

    /**
     * @return {number}
     */
    maximumBoundary: function()
    {
        return this._maximumBoundaries;
    },

    /**
     * @return {number}
     */
    minimumBoundary: function()
    {
        return this._minimumBoundaries;
    },

    /**
     * @return {number}
     */
    zeroTime: function()
    {
        return 0;
    },

    /**
     * @return {number}
     */
    boundarySpan: function()
    {
        return this._maximumBoundaries - this._minimumBoundaries;
    }
}

/**
 * @constructor
 * @implements {WebInspector.TimelineGrid.Calculator}
 */
WebInspector.FlameChart.OverviewCalculator = function()
{
}

WebInspector.FlameChart.OverviewCalculator.prototype = {
    /**
     * @param {WebInspector.FlameChart} flameChart
     */
    _updateBoundaries: function(flameChart)
    {
        this._minimumBoundaries = 0;
        var totalTime = flameChart._timelineData().totalTime;
        this._maximumBoundaries = totalTime;
        this._xScaleFactor = flameChart._canvas.width / totalTime;
    },

    /**
     * @param {number} time
     * @return {number}
     */
    computePosition: function(time)
    {
        return (time - this._minimumBoundaries) * this._xScaleFactor;
    },

    /**
     * @param {number} value
     * @return {string}
     */
    formatTime: function(value)
    {
        return Number.secondsToString((value + this._minimumBoundaries) / 1000);
    },

    /**
     * @return {number}
     */
    maximumBoundary: function()
    {
        return this._maximumBoundaries;
    },

    /**
     * @return {number}
     */
    minimumBoundary: function()
    {
        return this._minimumBoundaries;
    },

    /**
     * @return {number}
     */
    zeroTime: function()
    {
        return this._minimumBoundaries;
    },

    /**
     * @return {number}
     */
    boundarySpan: function()
    {
        return this._maximumBoundaries - this._minimumBoundaries;
    }
}

WebInspector.FlameChart.Events = {
    EntrySelected: "EntrySelected"
}

/**
 * @constructor
 */
WebInspector.FlameChart.ColorGenerator = function()
{
    this._colorPairs = {};
    this._colorIndexes = [];
    this._currentColorIndex = 0;
    this._colorPairForID("(idle)::0", 50);
    this._colorPairForID("(program)::0", 50);
    this._colorPairForID("(garbage collector)::0", 50);
}

WebInspector.FlameChart.ColorGenerator.prototype = {
    /**
     * @param {!string} id
     * @param {number=} sat
     */
    _colorPairForID: function(id, sat)
    {
        if (typeof sat !== "number")
            sat = 100;
        var colorPairs = this._colorPairs;
        var colorPair = colorPairs[id];
        if (!colorPair) {
            colorPairs[id] = colorPair = this._createPair(this._currentColorIndex++, sat);
            this._colorIndexes[colorPair.index] = colorPair;
        }
        return colorPair;
    },

    /**
     * @param {!number} index
     */
    _colorPairForIndex: function(index)
    {
        return this._colorIndexes[index];
    },

    /**
     * @param {!number} index
     * @param {!number} sat
     */
    _createPair: function(index, sat)
    {
        var hue = (index * 7 + 12 * (index % 2)) % 360;
        return {index: index, highlighted: "hsla(" + hue + ", " + sat + "%, 33%, 0.7)", normal: "hsla(" + hue + ", " + sat + "%, 66%, 0.7)"}
    }
}

/**
 * @constructor
 * @param {!Object} colorPair
 * @param {!number} depth
 * @param {!number} duration
 * @param {!number} startTime
 * @param {Object} node
 */
WebInspector.FlameChart.Entry = function(colorPair, depth, duration, startTime, node)
{
    this.colorPair = colorPair;
    this.depth = depth;
    this.duration = duration;
    this.startTime = startTime;
    this.node = node;
    this.selfTime = 0;
}

WebInspector.FlameChart.prototype = {
    _timelineData: function()
    {
        return this._dataProvider.timelineData(WebInspector.FlameChart._colorGenerator);
    },

    /**
     * @param {!number} timeLeft
     * @param {!number} timeRight
     */
    selectRange: function(timeLeft, timeRight)
    {
        this._overviewGrid.setWindow(timeLeft / this._totalTime, timeRight / this._totalTime);
    },

    _onWindowChanged: function(event)
    {
        this._scheduleUpdate();
    },

    _startCanvasDragging: function(event)
    {
        if (!this._timelineData())
            return false;
        this._isDragging = true;
        this._wasDragged = false;
        this._dragStartPoint = event.pageX;
        this._dragStartWindowLeft = this._windowLeft;
        this._dragStartWindowRight = this._windowRight;

        return true;
    },

    _canvasDragging: function(event)
    {
        var pixelShift = this._dragStartPoint - event.pageX;
        var windowShift = pixelShift / this._totalPixels;

        var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift);
        if (windowLeft === this._windowLeft)
            return;
        windowShift = windowLeft - this._dragStartWindowLeft;

        var windowRight = Math.min(1, this._dragStartWindowRight + windowShift);
        if (windowRight === this._windowRight)
            return;
        windowShift = windowRight - this._dragStartWindowRight;
        this._overviewGrid.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift);
        this._wasDragged = true;
    },

    _endCanvasDragging: function()
    {
        this._isDragging = false;
    },

    _onMouseMove: function(event)
    {
        if (this._isDragging)
            return;

        var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);

        if (this._highlightedEntryIndex === entryIndex)
            return;

        if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
            this._canvas.style.cursor = "default";
        else
            this._canvas.style.cursor = "pointer";

        this._highlightedEntryIndex = entryIndex;
        this._scheduleUpdate();
    },

    _onClick: function(e)
    {
        // onClick comes after dragStart and dragEnd events.
        // So if there was drag (mouse move) in the middle of that events
        // we skip the click. Otherwise we jump to the sources.
        if (this._wasDragged)
            return;
        if (this._highlightedEntryIndex === -1)
            return;
        var data = this._dataProvider.entryData(this._highlightedEntryIndex);
        this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, data);
    },

    _onMouseWheel: function(e)
    {
        if (e.wheelDeltaY) {
            const zoomFactor = 1.1;
            const mouseWheelZoomSpeed = 1 / 120;

            var zoom = Math.pow(zoomFactor, -e.wheelDeltaY * mouseWheelZoomSpeed);
            var overviewReference = (this._pixelWindowLeft + e.offsetX - this._paddingLeft) / this._totalPixels;
            this._overviewGrid.zoom(zoom, overviewReference);
        } else {
            var shift = Number.constrain(-1 * this._windowWidth / 4 * e.wheelDeltaX / 120, -this._windowLeft, 1 - this._windowRight);
            this._overviewGrid.setWindow(this._windowLeft + shift, this._windowRight + shift);
        }
    },

    /**
     * @param {!number} x
     * @param {!number} y
     */
    _coordinatesToEntryIndex: function(x, y)
    {
        var timelineData = this._timelineData();
        if (!timelineData)
            return -1;
        var timelineEntries = timelineData.entries;
        var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime;
        var cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);

        for (var i = 0; i < timelineEntries.length; ++i) {
            if (cursorTime < timelineEntries[i].startTime)
                return -1;
            if (cursorTime < (timelineEntries[i].startTime + timelineEntries[i].duration)
                && cursorLevel === timelineEntries[i].depth)
                return i;
        }
        return -1;
    },

    onResize: function()
    {
        this._updateOverviewCanvas = true;
        this._scheduleUpdate();
    },

    _drawOverviewCanvas: function(width, height)
    {
        if (!this._timelineData())
            return;

        var timelineEntries = this._timelineData().entries;

        var drawData = new Uint8Array(width);
        var scaleFactor = width / this._totalTime;

        for (var entryIndex = 0; entryIndex < timelineEntries.length; ++entryIndex) {
            var entry = timelineEntries[entryIndex];
            var start = Math.floor(entry.startTime * scaleFactor);
            var finish = Math.floor((entry.startTime + entry.duration) * scaleFactor);
            for (var x = start; x < finish; ++x)
                drawData[x] = Math.max(drawData[x], entry.depth + 1);
        }

        var ratio = window.devicePixelRatio;
        var canvasWidth = width * ratio;
        var canvasHeight = height * ratio;
        this._overviewCanvas.width = canvasWidth;
        this._overviewCanvas.height = canvasHeight;
        this._overviewCanvas.style.width = width + "px";
        this._overviewCanvas.style.height = height + "px";

        var context = this._overviewCanvas.getContext("2d");

        var yScaleFactor = canvasHeight / (this._timelineData().maxStackDepth * 1.1);
        context.lineWidth = 1;
        context.translate(0.5, 0.5);
        context.strokeStyle = "rgba(20,0,0,0.4)";
        context.fillStyle = "rgba(214,225,254,0.8)";
        context.moveTo(-1, canvasHeight - 1);
        if (drawData)<commit_msg>FlameChart: eliminate WebInspector.FlameChart.Entry class.<commit_after>/*
 * Copyright (C) 2013 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.
 */

/**
 * @constructor
 * @extends {WebInspector.View}
 * @param {WebInspector.FlameChartDataProvider} dataProvider
 */
WebInspector.FlameChart = function(dataProvider)
{
    WebInspector.View.call(this);
    this.registerRequiredCSS("flameChart.css");
    this.element.className = "fill";
    this.element.id = "cpu-flame-chart";

    this._overviewContainer = this.element.createChild("div", "overview-container");
    this._overviewGrid = new WebInspector.OverviewGrid("flame-chart");
    this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas");
    this._overviewContainer.appendChild(this._overviewGrid.element);
    this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator();
    this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);

    this._chartContainer = this.element.createChild("div", "chart-container");
    this._timelineGrid = new WebInspector.TimelineGrid();
    this._chartContainer.appendChild(this._timelineGrid.element);
    this._calculator = new WebInspector.FlameChart.Calculator();

    this._canvas = this._chartContainer.createChild("canvas");
    this._canvas.addEventListener("mousemove", this._onMouseMove.bind(this));
    WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize");

    this._entryInfo = this._chartContainer.createChild("div", "entry-info");

    this._dataProvider = dataProvider;
    this._windowLeft = 0.0;
    this._windowRight = 1.0;
    this._barHeight = 15;
    this._minWidth = 1;
    this._paddingLeft = 15;
    this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
    this._canvas.addEventListener("click", this._onClick.bind(this), false);
    this._linkifier = new WebInspector.Linkifier();
    this._highlightedEntryIndex = -1;

    if (!WebInspector.FlameChart._colorGenerator)
        WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator();
}

/**
 * @constructor
 */
WebInspector.FlameChartDataProvider = function(cpuProfileView)
{
    this._cpuProfileView = cpuProfileView;
}

WebInspector.FlameChartDataProvider.prototype = {
    /**
     * @param {WebInspector.FlameChart.ColorGenerator} colorGenerator
     * @return {Object}
     */
    timelineData: function(colorGenerator)
    {
        return this._timelineData || this._calculateTimelineData(colorGenerator);
    },

    /**
     * @param {WebInspector.FlameChart.ColorGenerator} colorGenerator
     * @return {Object}
     */
    _calculateTimelineData: function(colorGenerator)
    {
        if (!this._cpuProfileView.profileHead)
            return null;

        var samples = this._cpuProfileView.samples;
        var idToNode = this._cpuProfileView._idToNode;
        var gcNode = this._cpuProfileView._gcNode;
        var samplesCount = samples.length;
        var samplingInterval = this._cpuProfileView.samplingIntervalMs;

        var index = 0;

        var openIntervals = [];
        var stackTrace = [];
        var colorEntryIndexes = [];
        var maxDepth = 5; // minimum stack depth for the case when we see no activity.
        var depth = 0;

        /**
         * @constructor
         * @param {!Object} colorPair
         * @param {!number} depth
         * @param {!number} duration
         * @param {!number} startTime
         * @param {Object} node
         */
        function ChartEntry(colorPair, depth, duration, startTime, node)
        {
            this.colorPair = colorPair;
            this.depth = depth;
            this.duration = duration;
            this.startTime = startTime;
            this.node = node;
            this.selfTime = 0;
        }
        var entries = /** @type {Array.<!ChartEntry>} */ ([]);

        for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) {
            var node = idToNode[samples[sampleIndex]];
            stackTrace.length = 0;
            while (node) {
                stackTrace.push(node);
                node = node.parent;
            }
            stackTrace.pop(); // Remove (root) node

            maxDepth = Math.max(maxDepth, depth);
            depth = 0;
            node = stackTrace.pop();
            var intervalIndex;

            // GC samples have no stack, so we just put GC node on top of the last recoreded sample.
            if (node === gcNode) {
                while (depth < openIntervals.length) {
                    intervalIndex = openIntervals[depth].index;
                    entries[intervalIndex].duration += samplingInterval;
                    ++depth;
                }
                // If previous stack is also GC then just continue.
                if (openIntervals.length > 0 && openIntervals.peekLast().node === node) {
                    entries[intervalIndex].selfTime += samplingInterval;
                    continue;
                }
            }

            while (node && depth < openIntervals.length && node === openIntervals[depth].node) {
                intervalIndex = openIntervals[depth].index;
                entries[intervalIndex].duration += samplingInterval;
                node = stackTrace.pop();
                ++depth;
            }
            if (depth < openIntervals.length)
                openIntervals.length = depth;
            if (!node) {
                entries[intervalIndex].selfTime += samplingInterval;
                continue;
            }

            while (node) {
                var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
                var indexesForColor = colorEntryIndexes[colorPair.index];
                if (!indexesForColor)
                    indexesForColor = colorEntryIndexes[colorPair.index] = [];

                var entry = new ChartEntry(colorPair, depth, samplingInterval, sampleIndex * samplingInterval, node);
                indexesForColor.push(entries.length);
                entries.push(entry);
                openIntervals.push({node: node, index: index});
                ++index;

                node = stackTrace.pop();
                ++depth;
            }
            entries[entries.length - 1].selfTime += samplingInterval;
        }

        var entryNodes = new Array(entries.length);
        var entryColorIndexes = new Uint16Array(entries.length);
        var entryLevels = new Uint8Array(entries.length);
        var entryTotalTimes = new Float32Array(entries.length);
        var entrySelfTimes = new Float32Array(entries.length);
        var entryOffsets = new Float32Array(entries.length);
        var entryTitles = new Array(entries.length);
        var entryDeoptFlags = new Uint8Array(entries.length);

        for (var i = 0; i < entries.length; ++i) {
            var entry = entries[i];
            entryNodes[i] = entry.node;
            entryColorIndexes[i] = colorPair.index;
            entryLevels[i] = entry.depth;
            entryTotalTimes[i] = entry.duration;
            entrySelfTimes[i] = entry.selfTime;
            entryOffsets[i] = entry.startTime;
            entryTitles[i] = entry.node.functionName;
            var reason = entry.node.deoptReason;
            entryDeoptFlags[i] = (reason && reason !== "no reason");
        }

        this._timelineData = {
            maxStackDepth: Math.max(maxDepth, depth),
            totalTime: this._cpuProfileView.profileHead.totalTime,
            entryNodes: entryNodes,
            entryColorIndexes: entryColorIndexes,
            entryLevels: entryLevels,
            entryTotalTimes: entryTotalTimes,
            entrySelfTimes: entrySelfTimes,
            entryOffsets: entryOffsets,
            colorEntryIndexes: colorEntryIndexes,
            entryTitles: entryTitles,
            entryDeoptFlags: entryDeoptFlags
        };

        return this._timelineData;
    },

    /**
     * @param {number} ms
     */
    _millisecondsToString: function(ms)
    {
        if (ms === 0)
            return "0";
        if (ms < 1000)
            return WebInspector.UIString("%.1f\u2009ms", ms);
        return Number.secondsToString(ms / 1000, true);
    },

    /**
     * @param {number} entryIndex
     */
    prepareHighlightedEntryInfo: function(entryIndex)
    {
        var timelineData = this._timelineData;
        var node = timelineData.entryNodes[entryIndex];
        if (!node)
            return null;

        var entryInfo = [];
        function pushEntryInfoRow(title, text)
        {
            var row = {};
            row.title = title;
            row.text = text;
            entryInfo.push(row);
        }

        pushEntryInfoRow(WebInspector.UIString("Name"), timelineData.entryTitles[entryIndex]);
        var selfTime = this._millisecondsToString(timelineData.entrySelfTimes[entryIndex]);
        var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[entryIndex]);
        pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime);
        pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime);
        if (node.url)
            pushEntryInfoRow(WebInspector.UIString("URL"), node.url + ":" + node.lineNumber);
        pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true));
        pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true));
        if (node.deoptReason && node.deoptReason !== "no reason")
            pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason);

        return entryInfo;
    },

    /**
     * @param {number} entryIndex
     * @return {boolean}
     */
    canJumpToEntry: function(entryIndex)
    {
        return this._timelineData.entryNodes[entryIndex].scriptId !== "0";
    },

    /**
     * @param {number} entryIndex
     * @return {Object}
     */
    entryData: function(entryIndex)
    {
        return this._timelineData.entryNodes[entryIndex];
    }
}

/**
 * @constructor
 * @implements {WebInspector.TimelineGrid.Calculator}
 */
WebInspector.FlameChart.Calculator = function()
{
}

WebInspector.FlameChart.Calculator.prototype = {
    /**
     * @param {WebInspector.FlameChart} flameChart
     */
    _updateBoundaries: function(flameChart)
    {
        function log10(x)
        {
            return Math.log(x) / Math.LN10;
        }
        this._decimalDigits = Math.max(0, -Math.floor(log10(flameChart._timelineGrid.gridSliceTime * 1.01)));
        var totalTime = flameChart._timelineData().totalTime;
        this._minimumBoundaries = flameChart._windowLeft * totalTime;
        this._maximumBoundaries = flameChart._windowRight * totalTime;
        this.paddingLeft = flameChart._paddingLeft;
        this._width = flameChart._canvas.width - this.paddingLeft;
        this._timeToPixel = this._width / this.boundarySpan();
    },

    /**
     * @param {number} time
     * @return {number}
     */
    computePosition: function(time)
    {
        return (time - this._minimumBoundaries) * this._timeToPixel + this.paddingLeft;
    },

    /**
     * @param {number} value
     * @return {string}
     */
    formatTime: function(value)
    {
        var format = "%." + this._decimalDigits + "f\u2009ms";
        return WebInspector.UIString(format, value + this._minimumBoundaries);
    },

    /**
     * @return {number}
     */
    maximumBoundary: function()
    {
        return this._maximumBoundaries;
    },

    /**
     * @return {number}
     */
    minimumBoundary: function()
    {
        return this._minimumBoundaries;
    },

    /**
     * @return {number}
     */
    zeroTime: function()
    {
        return 0;
    },

    /**
     * @return {number}
     */
    boundarySpan: function()
    {
        return this._maximumBoundaries - this._minimumBoundaries;
    }
}

/**
 * @constructor
 * @implements {WebInspector.TimelineGrid.Calculator}
 */
WebInspector.FlameChart.OverviewCalculator = function()
{
}

WebInspector.FlameChart.OverviewCalculator.prototype = {
    /**
     * @param {WebInspector.FlameChart} flameChart
     */
    _updateBoundaries: function(flameChart)
    {
        this._minimumBoundaries = 0;
        var totalTime = flameChart._timelineData().totalTime;
        this._maximumBoundaries = totalTime;
        this._xScaleFactor = flameChart._canvas.width / totalTime;
    },

    /**
     * @param {number} time
     * @return {number}
     */
    computePosition: function(time)
    {
        return (time - this._minimumBoundaries) * this._xScaleFactor;
    },

    /**
     * @param {number} value
     * @return {string}
     */
    formatTime: function(value)
    {
        return Number.secondsToString((value + this._minimumBoundaries) / 1000);
    },

    /**
     * @return {number}
     */
    maximumBoundary: function()
    {
        return this._maximumBoundaries;
    },

    /**
     * @return {number}
     */
    minimumBoundary: function()
    {
        return this._minimumBoundaries;
    },

    /**
     * @return {number}
     */
    zeroTime: function()
    {
        return this._minimumBoundaries;
    },

    /**
     * @return {number}
     */
    boundarySpan: function()
    {
        return this._maximumBoundaries - this._minimumBoundaries;
    }
}

WebInspector.FlameChart.Events = {
    EntrySelected: "EntrySelected"
}

/**
 * @constructor
 */
WebInspector.FlameChart.ColorGenerator = function()
{
    this._colorPairs = {};
    this._colorIndexes = [];
    this._currentColorIndex = 0;
    this._colorPairForID("(idle)::0", 50);
    this._colorPairForID("(program)::0", 50);
    this._colorPairForID("(garbage collector)::0", 50);
}

WebInspector.FlameChart.ColorGenerator.prototype = {
    /**
     * @param {!string} id
     * @param {number=} sat
     */
    _colorPairForID: function(id, sat)
    {
        if (typeof sat !== "number")
            sat = 100;
        var colorPairs = this._colorPairs;
        var colorPair = colorPairs[id];
        if (!colorPair) {
            colorPairs[id] = colorPair = this._createPair(this._currentColorIndex++, sat);
            this._colorIndexes[colorPair.index] = colorPair;
        }
        return colorPair;
    },

    /**
     * @param {!number} index
     */
    _colorPairForIndex: function(index)
    {
        return this._colorIndexes[index];
    },

    /**
     * @param {!number} index
     * @param {!number} sat
     */
    _createPair: function(index, sat)
    {
        var hue = (index * 7 + 12 * (index % 2)) % 360;
        return {index: index, highlighted: "hsla(" + hue + ", " + sat + "%, 33%, 0.7)", normal: "hsla(" + hue + ", " + sat + "%, 66%, 0.7)"}
    }
}

WebInspector.FlameChart.prototype = {
    _timelineData: function()
    {
        return this._dataProvider.timelineData(WebInspector.FlameChart._colorGenerator);
    },

    /**
     * @param {!number} timeLeft
     * @param {!number} timeRight
     */
    selectRange: function(timeLeft, timeRight)
    {
        this._overviewGrid.setWindow(timeLeft / this._totalTime, timeRight / this._totalTime);
    },

    _onWindowChanged: function(event)
    {
        this._scheduleUpdate();
    },

    _startCanvasDragging: function(event)
    {
        if (!this._timelineData())
            return false;
        this._isDragging = true;
        this._wasDragged = false;
        this._dragStartPoint = event.pageX;
        this._dragStartWindowLeft = this._windowLeft;
        this._dragStartWindowRight = this._windowRight;

        return true;
    },

    _canvasDragging: function(event)
    {
        var pixelShift = this._dragStartPoint - event.pageX;
        var windowShift = pixelShift / this._totalPixels;

        var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift);
        if (windowLeft === this._windowLeft)
            return;
        windowShift = windowLeft - this._dragStartWindowLeft;

        var windowRight = Math.min(1, this._dragStartWindowRight + windowShift);
        if (windowRight === this._windowRight)
            return;
        windowShift = windowRight - this._dragStartWindowRight;
        this._overviewGrid.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift);
        this._wasDragged = true;
    },

    _endCanvasDragging: function()
    {
        this._isDragging = false;
    },

    _onMouseMove: function(event)
    {
        if (this._isDragging)
            return;

        var entryIndex = this._coordinatesToEntryIndex(event.offsetX, event.offsetY);

        if (this._highlightedEntryIndex === entryIndex)
            return;

        if (entryIndex === -1 || !this._dataProvider.canJumpToEntry(entryIndex))
            this._canvas.style.cursor = "default";
        else
            this._canvas.style.cursor = "pointer";

        this._highlightedEntryIndex = entryIndex;
        this._scheduleUpdate();
    },

    _onClick: function(e)
    {
        // onClick comes after dragStart and dragEnd events.
        // So if there was drag (mouse move) in the middle of that events
        // we skip the click. Otherwise we jump to the sources.
        if (this._wasDragged)
            return;
        if (this._highlightedEntryIndex === -1)
            return;
        var data = this._dataProvider.entryData(this._highlightedEntryIndex);
        this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, data);
    },

    _onMouseWheel: function(e)
    {
        if (e.wheelDeltaY) {
            const zoomFactor = 1.1;
            const mouseWheelZoomSpeed = 1 / 120;

            var zoom = Math.pow(zoomFactor, -e.wheelDeltaY * mouseWheelZoomSpeed);
            var overviewReference = (this._pixelWindowLeft + e.offsetX - this._paddingLeft) / this._totalPixels;
            this._overviewGrid.zoom(zoom, overviewReference);
        } else {
            var shift = Number.constrain(-1 * this._windowWidth / 4 * e.wheelDeltaX / 120, -this._windowLeft, 1 - this._windowRight);
            this._overviewGrid.setWindow(this._windowLeft + shift, this._windowRight + shift);
        }
    },

    /**
     * @param {!number} x
     * @param {!number} y
     */
    _coordinatesToEntryIndex: function(x, y)
    {
        var timelineData = this._timelineData();
        if (!timelineData)
            return -1;
        var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime;
        var cursorLevel = Math.floor((this._canvas.height / window.devicePixelRatio - y) / this._barHeight);

        var entryOffsets = timelineData.entryOffsets;
        var entryTotalTimes = timelineData.entryTotalTimes;
        var entryLevels = timelineData.entryLevels;
        var length = entryOffsets.length;
        for (var i = 0; i < length; ++i) {
            if (cursorTime < entryOffsets[i])
                return -1;
            if (cursorTime < (entryOffsets[i] + entryTotalTimes[i])
                && cursorLevel === entryLevels[i])
                return i;
        }
        return -1;
    },

    onResize: function()
    {
        this._updateOverviewCanvas = true;
        this._scheduleUpdate();
    },

    _drawOverviewCanvas: function(width, height)
    {
        var timelineData = this._timelineData();
        if (!timelineData)
            return;

        var entryOffsets = timelineData.entryOffsets;
        var entryTotalTimes = timelineData.entryTotalTimes;
        var entryLevels = timelineData.entryLevels;
        var length = entryOffsets.length;

        var drawData = new Uint8Array(width);
        var scaleFactor = width / this._totalTime;

        for (var entryIndex = 0; entryIndex < length; ++entryIndex) {
            var start = Math.floor(entryOffsets[i] * scaleFactor);
            var finish = Math.floor((entryOffsets[i] + entryTotalTimes[i]) * scaleFactor);
            for (var x = start; x < finish; ++x)
                drawData[x] = Math.max(drawData[x], entryLevels[i] + 1);
        }

        var ratio = window.devicePixelRatio;
        var canvasWidth = width * ratio;
        var canvasHeight = height * ratio;
        this._overviewCanvas.width = canvasWidth;
        this._overviewCanvas.height = canvasHeight;
        this._overviewCanvas.style.width = width + "px";
        this._overviewCanvas.style.height = height + "px";

        var context = this._overviewCanvas.getContext("2d");

        var yScaleFactor = canvasHeight / (this._timelineData().maxStackDepth * 1.1);
        context.lineWidth = 1;
        context.translate(0.5, 0.5);
        context.strokeStyle = "rgba(20,0,0,0.4)";
        context.fillStyle = "rgba(214,225,254,0.8)";
        context.moveTo(-1, canvasHeight - 1);
        if (drawData)
{'fraction_non_alphanumeric': 0.11166101694915254, 'fraction_numerical': 0.007005649717514125, 'mean_word_length': 2.4123226403454656, 'pattern_counts': {'":': 6, '<': 27, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1353204', 'n_tokens_mistral': 12903, 'n_tokens_neox': 12220, 'n_words': 2654}