DOMCountersGraph.js   [plain text]


/*
 * 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.MemoryStatistics}
 * @param {WebInspector.TimelinePanel} timelinePanel
 * @param {WebInspector.TimelineModel} model
 * @param {number} sidebarWidth
 */
WebInspector.DOMCountersGraph = function(timelinePanel, model, sidebarWidth)
{
    WebInspector.MemoryStatistics.call(this, timelinePanel, model, sidebarWidth);
}

/**
 * @constructor
 * @extends {WebInspector.CounterUIBase}
 * @param {WebInspector.DOMCountersGraph} memoryCountersPane
 * @param {string} title
 * @param {string} currentValueLabel
 * @param {Array.<number>} rgb
 * @param {function(WebInspector.DOMCountersGraph.Counter):number} valueGetter
 */
WebInspector.DOMCounterUI = function(memoryCountersPane, title, currentValueLabel, rgb, valueGetter)
{
    var swatchColor = "rgb(" + rgb.join(",") + ")";
    WebInspector.CounterUIBase.call(this, memoryCountersPane, title, swatchColor, valueGetter)
    this._range = this._swatch.element.createChild("span");

    this._value = memoryCountersPane._currentValuesBar.createChild("span", "memory-counter-value");
    this._value.style.color = swatchColor;
    this._currentValueLabel = currentValueLabel;

    this.graphColor = "rgba(" + rgb.join(",") + ",0.8)";
    this.graphYValues = [];
}

/**
 * @constructor
 * @extends {WebInspector.MemoryStatistics.Counter}
 * @param {number} time
 * @param {number} documentCount
 * @param {number} nodeCount
 * @param {number} listenerCount
 */
WebInspector.DOMCountersGraph.Counter = function(time, documentCount, nodeCount, listenerCount)
{
    WebInspector.MemoryStatistics.Counter.call(this, time);
    this.documentCount = documentCount;
    this.nodeCount = nodeCount;
    this.listenerCount = listenerCount;
}

WebInspector.DOMCounterUI.prototype = {
    /**
     * @param {number} minValue
     * @param {number} maxValue
     */
    setRange: function(minValue, maxValue)
    {
        this._range.textContent = WebInspector.UIString("[ %d - %d ]", minValue, maxValue);
    },

    updateCurrentValue: function(countersEntry)
    {
        this._value.textContent =  WebInspector.UIString(this._currentValueLabel, this.valueGetter(countersEntry));
    },

    clearCurrentValueAndMarker: function(ctx)
    {
        this._value.textContent = "";
        this.restoreImageUnderMarker(ctx);
    },

    /**
     * @param {CanvasRenderingContext2D} ctx
     * @param {number} x
     * @param {number} y
     * @param {number} radius
     */
    saveImageUnderMarker: function(ctx, x, y, radius)
    {
        const w = radius + 1;
        var imageData = ctx.getImageData(x - w, y - w, 2 * w, 2 * w);
        this._imageUnderMarker = {
            x: x - w,
            y: y - w,
            imageData: imageData
        };
    },

    /**
     * @param {CanvasRenderingContext2D} ctx
     */
    restoreImageUnderMarker: function(ctx)
    {
        if (!this.visible)
            return;
        if (this._imageUnderMarker)
            ctx.putImageData(this._imageUnderMarker.imageData, this._imageUnderMarker.x, this._imageUnderMarker.y);
        this.discardImageUnderMarker();
    },

    discardImageUnderMarker: function()
    {
        delete this._imageUnderMarker;
    },

    __proto__: WebInspector.CounterUIBase.prototype
}


WebInspector.DOMCountersGraph.prototype = {
    _createCurrentValuesBar: function()
    {
        this._currentValuesBar = this._canvasContainer.createChild("div");
        this._currentValuesBar.id = "counter-values-bar";
        this._canvasContainer.addStyleClass("dom-counters");
    },

    /**
     * @return {Array.<WebInspector.DOMCounterUI>}
     */
    _createCounterUIList: function()
    {
        function getDocumentCount(entry)
        {
            return entry.documentCount;
        }
        function getNodeCount(entry)
        {
            return entry.nodeCount;
        }
        function getListenerCount(entry)
        {
            return entry.listenerCount;
        }
        return [
            new WebInspector.DOMCounterUI(this, "Document Count", "Documents: %d", [100, 0, 0], getDocumentCount),
            new WebInspector.DOMCounterUI(this, "DOM Node Count", "Nodes: %d", [0, 100, 0], getNodeCount),
            new WebInspector.DOMCounterUI(this, "Event Listener Count", "Listeners: %d", [0, 0, 100], getListenerCount)
        ];
    },

    _canvasHeight: function()
    {
        return this._canvasContainer.offsetHeight - this._currentValuesBar.offsetHeight;
    },

    /**
     * @param {WebInspector.Event} event
     */
    _onRecordAdded: function(event)
    {
        function addStatistics(record)
        {
            var counters = record["counters"];
            if (!counters)
                return;
            this._counters.push(new WebInspector.DOMCountersGraph.Counter(
                record.endTime || record.startTime,
                counters["documents"],
                counters["nodes"],
                counters["jsEventListeners"]
            ));
        }
        WebInspector.TimelinePresentationModel.forAllRecords([event.data], null, addStatistics.bind(this));
    },

    _draw: function()
    {
        WebInspector.MemoryStatistics.prototype._draw.call(this);
        for (var i = 0; i < this._counterUI.length; i++)
            this._drawGraph(this._counterUI[i]);
    },

    /**
     * @param {CanvasRenderingContext2D} ctx
     */
    _restoreImageUnderMarker: function(ctx)
    {
        for (var i = 0; i < this._counterUI.length; i++) {
            var counterUI = this._counterUI[i];
            if (!counterUI.visible)
                continue;
            counterUI.restoreImageUnderMarker(ctx);
        }
    },

    /**
     * @param {CanvasRenderingContext2D} ctx
     * @param {number} x
     * @param {number} index
     */
    _saveImageUnderMarker: function(ctx, x, index)
    {
        const radius = 2;
        for (var i = 0; i < this._counterUI.length; i++) {
            var counterUI = this._counterUI[i];
            if (!counterUI.visible)
                continue;
            var y = counterUI.graphYValues[index];
            counterUI.saveImageUnderMarker(ctx, x, y, radius);
        }
    },

    /**
     * @param {CanvasRenderingContext2D} ctx
     * @param {number} x
     * @param {number} index
     */
    _drawMarker: function(ctx, x, index)
    {
        this._saveImageUnderMarker(ctx, x, index);
        const radius = 2;
        for (var i = 0; i < this._counterUI.length; i++) {
            var counterUI = this._counterUI[i];
            if (!counterUI.visible)
                continue;
            var y = counterUI.graphYValues[index];
            ctx.beginPath();
            ctx.arc(x, y, radius, 0, Math.PI * 2, true);
            ctx.lineWidth = 1;
            ctx.fillStyle = counterUI.graphColor;
            ctx.strokeStyle = counterUI.graphColor;
            ctx.fill();
            ctx.stroke();
            ctx.closePath();
        }
    },

    /**
     * @param {WebInspector.CounterUIBase} counterUI
     */
    _drawGraph: function(counterUI)
    {
        var canvas = this._canvas;
        var ctx = canvas.getContext("2d");
        var width = canvas.width;
        var height = this._clippedHeight;
        var originY = this._originY;
        var valueGetter = counterUI.valueGetter;

        if (!this._counters.length)
            return;

        var maxValue;
        var minValue;
        for (var i = this._minimumIndex; i <= this._maximumIndex; i++) {
            var value = valueGetter(this._counters[i]);
            if (minValue === undefined || value < minValue)
                minValue = value;
            if (maxValue === undefined || value > maxValue)
                maxValue = value;
        }

        counterUI.setRange(minValue, maxValue);

        if (!counterUI.visible)
            return;

        var yValues = counterUI.graphYValues;
        yValues.length = this._counters.length;

        var maxYRange = maxValue - minValue;
        var yFactor = maxYRange ? height / (maxYRange) : 1;

        ctx.beginPath();
        var currentY = originY + (height - (valueGetter(this._counters[this._minimumIndex]) - minValue) * yFactor);
        ctx.moveTo(0, currentY);
        for (var i = this._minimumIndex; i <= this._maximumIndex; i++) {
             var x = this._counters[i].x;
             ctx.lineTo(x, currentY);
             currentY = originY + (height - (valueGetter(this._counters[i]) - minValue) * yFactor);
             ctx.lineTo(x, currentY);

             yValues[i] = currentY;
        }
        ctx.lineTo(width, currentY);
        ctx.lineWidth = 1;
        ctx.strokeStyle = counterUI.graphColor;
        ctx.stroke();
        ctx.closePath();
    },

    _discardImageUnderMarker: function()
    {
        for (var i = 0; i < this._counterUI.length; i++)
            this._counterUI[i].discardImageUnderMarker();
    },

    __proto__: WebInspector.MemoryStatistics.prototype
}