Home | History | Annotate | Download | only in timeline
      1 /*
      2  * Copyright (C) 2012 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 /**
     32  * @constructor
     33  * @extends {WebInspector.SplitView}
     34  * @param {string} title
     35  * @param {!WebInspector.TimelineModeViewDelegate} delegate
     36  * @param {!WebInspector.TimelineModel} model
     37  */
     38 WebInspector.CountersGraph = function(title, delegate, model)
     39 {
     40     WebInspector.SplitView.call(this, true, false);
     41 
     42     this.element.id = "memory-graphs-container";
     43 
     44     this._delegate = delegate;
     45     this._model = model;
     46     this._calculator = new WebInspector.TimelineCalculator(this._model);
     47 
     48     this._graphsContainer = this.mainElement();
     49     this._createCurrentValuesBar();
     50     this._canvasView = new WebInspector.VBoxWithResizeCallback(this._resize.bind(this));
     51     this._canvasView.show(this._graphsContainer);
     52     this._canvasContainer = this._canvasView.element;
     53     this._canvasContainer.id = "memory-graphs-canvas-container";
     54     this._canvas = this._canvasContainer.createChild("canvas");
     55     this._canvas.id = "memory-counters-graph";
     56 
     57     this._canvasContainer.addEventListener("mouseover", this._onMouseMove.bind(this), true);
     58     this._canvasContainer.addEventListener("mousemove", this._onMouseMove.bind(this), true);
     59     this._canvasContainer.addEventListener("mouseout", this._onMouseOut.bind(this), true);
     60     this._canvasContainer.addEventListener("click", this._onClick.bind(this), true);
     61     // We create extra timeline grid here to reuse its event dividers.
     62     this._timelineGrid = new WebInspector.TimelineGrid();
     63     this._canvasContainer.appendChild(this._timelineGrid.dividersElement);
     64 
     65     // Populate sidebar
     66     this.sidebarElement().createChild("div", "sidebar-tree sidebar-tree-section").textContent = title;
     67     this._counters = [];
     68     this._counterUI = [];
     69 }
     70 
     71 WebInspector.CountersGraph.prototype = {
     72     _createCurrentValuesBar: function()
     73     {
     74         this._currentValuesBar = this._graphsContainer.createChild("div");
     75         this._currentValuesBar.id = "counter-values-bar";
     76     },
     77 
     78     /**
     79      * @param {string} uiName
     80      * @param {string} uiValueTemplate
     81      * @param {string} color
     82      * @return {!WebInspector.CountersGraph.Counter}
     83      */
     84     createCounter: function(uiName, uiValueTemplate, color)
     85     {
     86         var counter = new WebInspector.CountersGraph.Counter();
     87         this._counters.push(counter);
     88         this._counterUI.push(new WebInspector.CountersGraph.CounterUI(this, uiName, uiValueTemplate, color, counter));
     89         return counter;
     90     },
     91 
     92     /**
     93      * @return {!WebInspector.View}
     94      */
     95     view: function()
     96     {
     97         return this;
     98     },
     99 
    100     dispose: function()
    101     {
    102     },
    103 
    104     reset: function()
    105     {
    106         for (var i = 0; i < this._counters.length; ++i) {
    107             this._counters[i].reset();
    108             this._counterUI[i].reset();
    109         }
    110         this.refresh();
    111     },
    112 
    113     _resize: function()
    114     {
    115         var parentElement = this._canvas.parentElement;
    116         this._canvas.width = parentElement.clientWidth  * window.devicePixelRatio;
    117         this._canvas.height = parentElement.clientHeight * window.devicePixelRatio;
    118         var timelinePaddingLeft = 15;
    119         this._calculator.setDisplayWindow(timelinePaddingLeft, this._canvas.width);
    120         this.refresh();
    121     },
    122 
    123     /**
    124      * @param {number} startTime
    125      * @param {number} endTime
    126      */
    127     setWindowTimes: function(startTime, endTime)
    128     {
    129         this._calculator.setWindow(startTime, endTime);
    130         this.scheduleRefresh();
    131     },
    132 
    133     scheduleRefresh: function()
    134     {
    135         WebInspector.invokeOnceAfterBatchUpdate(this, this.refresh);
    136     },
    137 
    138     draw: function()
    139     {
    140         for (var i = 0; i < this._counters.length; ++i) {
    141             this._counters[i]._calculateVisibleIndexes(this._calculator);
    142             this._counters[i]._calculateXValues(this._canvas.width);
    143         }
    144         this._clear();
    145 
    146         for (var i = 0; i < this._counterUI.length; i++)
    147             this._counterUI[i]._drawGraph(this._canvas);
    148     },
    149 
    150     /**
    151      * @param {!Event} event
    152      */
    153     _onClick: function(event)
    154     {
    155         var x = event.x - this._canvasContainer.totalOffsetLeft();
    156         var minDistance = Infinity;
    157         var bestTime;
    158         for (var i = 0; i < this._counterUI.length; ++i) {
    159             var counterUI = this._counterUI[i];
    160             if (!counterUI.counter.times.length)
    161                 continue;
    162             var index = counterUI._recordIndexAt(x);
    163             var distance = Math.abs(x * window.devicePixelRatio - counterUI.counter.x[index]);
    164             if (distance < minDistance) {
    165                 minDistance = distance;
    166                 bestTime = counterUI.counter.times[index];
    167             }
    168         }
    169         if (bestTime !== undefined)
    170             this._revealRecordAt(bestTime);
    171     },
    172 
    173     /**
    174      * @param {number} time
    175      */
    176     _revealRecordAt: function(time)
    177     {
    178         var recordToReveal;
    179         /**
    180          * @param {!WebInspector.TimelineModel.Record} record
    181          * @return {boolean}
    182          * @this {WebInspector.CountersGraph}
    183          */
    184         function findRecordToReveal(record)
    185         {
    186             if (!this._model.isVisible(record))
    187                 return false;
    188             if (record.startTime() <= time && time <= record.endTime()) {
    189                 recordToReveal = record;
    190                 return true;
    191             }
    192             // If there is no record containing the time than use the latest one before that time.
    193             if (!recordToReveal || record.endTime() < time && recordToReveal.endTime() < record.endTime())
    194                 recordToReveal = record;
    195             return false;
    196         }
    197         this._model.forAllRecords(null, findRecordToReveal.bind(this));
    198         this._delegate.select(recordToReveal ? WebInspector.TimelineSelection.fromRecord(recordToReveal) : null);
    199     },
    200 
    201     /**
    202      * @param {!Event} event
    203      */
    204     _onMouseOut: function(event)
    205     {
    206         delete this._markerXPosition;
    207         this._clearCurrentValueAndMarker();
    208     },
    209 
    210     _clearCurrentValueAndMarker: function()
    211     {
    212         for (var i = 0; i < this._counterUI.length; i++)
    213             this._counterUI[i]._clearCurrentValueAndMarker();
    214     },
    215 
    216     /**
    217      * @param {!Event} event
    218      */
    219     _onMouseMove: function(event)
    220     {
    221         var x = event.x - this._canvasContainer.totalOffsetLeft();
    222         this._markerXPosition = x;
    223         this._refreshCurrentValues();
    224     },
    225 
    226     _refreshCurrentValues: function()
    227     {
    228         if (this._markerXPosition === undefined)
    229             return;
    230         for (var i = 0; i < this._counterUI.length; ++i)
    231             this._counterUI[i].updateCurrentValue(this._markerXPosition);
    232     },
    233 
    234     refresh: function()
    235     {
    236         this._timelineGrid.updateDividers(this._calculator);
    237         this.draw();
    238         this._refreshCurrentValues();
    239     },
    240 
    241     refreshRecords: function()
    242     {
    243     },
    244 
    245     _clear: function()
    246     {
    247         var ctx = this._canvas.getContext("2d");
    248         ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    249     },
    250 
    251     /**
    252      * @param {?WebInspector.TimelineModel.Record} record
    253      * @param {string=} regex
    254      * @param {boolean=} selectRecord
    255      */
    256     highlightSearchResult: function(record, regex, selectRecord)
    257     {
    258     },
    259 
    260     /**
    261      * @param {?WebInspector.TimelineSelection} selection
    262      */
    263     setSelection: function(selection)
    264     {
    265     },
    266 
    267     __proto__: WebInspector.SplitView.prototype
    268 }
    269 
    270 /**
    271  * @constructor
    272  */
    273 WebInspector.CountersGraph.Counter = function()
    274 {
    275     this.times = [];
    276     this.values = [];
    277 }
    278 
    279 WebInspector.CountersGraph.Counter.prototype = {
    280     /**
    281      * @param {number} time
    282      * @param {number} value
    283      */
    284     appendSample: function(time, value)
    285     {
    286         if (this.values.length && this.values.peekLast() === value)
    287             return;
    288         this.times.push(time);
    289         this.values.push(value);
    290     },
    291 
    292     reset: function()
    293     {
    294         this.times = [];
    295         this.values = [];
    296     },
    297 
    298     /**
    299      * @param {number} value
    300      */
    301     setLimit: function(value)
    302     {
    303         this._limitValue = value;
    304     },
    305 
    306     /**
    307      * @return {!{min: number, max: number}}
    308      */
    309     _calculateBounds: function()
    310     {
    311         var maxValue;
    312         var minValue;
    313         for (var i = this._minimumIndex; i <= this._maximumIndex; i++) {
    314             var value = this.values[i];
    315             if (minValue === undefined || value < minValue)
    316                 minValue = value;
    317             if (maxValue === undefined || value > maxValue)
    318                 maxValue = value;
    319         }
    320         minValue = minValue || 0;
    321         maxValue = maxValue || 1;
    322         if (this._limitValue) {
    323             if (maxValue > this._limitValue * 0.5)
    324                 maxValue = Math.max(maxValue, this._limitValue);
    325             minValue = Math.min(minValue, this._limitValue);
    326         }
    327         return { min: minValue, max: maxValue };
    328     },
    329 
    330     /**
    331      * @param {!WebInspector.TimelineCalculator} calculator
    332      */
    333     _calculateVisibleIndexes: function(calculator)
    334     {
    335         var start = calculator.minimumBoundary();
    336         var end = calculator.maximumBoundary();
    337 
    338         // Maximum index of element whose time <= start.
    339         this._minimumIndex = Number.constrain(this.times.upperBound(start) - 1, 0, this.times.length - 1);
    340 
    341         // Minimum index of element whose time >= end.
    342         this._maximumIndex = Number.constrain(this.times.lowerBound(end), 0, this.times.length - 1);
    343 
    344         // Current window bounds.
    345         this._minTime = start;
    346         this._maxTime = end;
    347     },
    348 
    349     /**
    350      * @param {number} width
    351      */
    352     _calculateXValues: function(width)
    353     {
    354         if (!this.values.length)
    355             return;
    356 
    357         var xFactor = width / (this._maxTime - this._minTime);
    358 
    359         this.x = new Array(this.values.length);
    360         for (var i = this._minimumIndex + 1; i <= this._maximumIndex; i++)
    361              this.x[i] = xFactor * (this.times[i] - this._minTime);
    362     }
    363 }
    364 
    365 /**
    366  * @constructor
    367  * @param {!WebInspector.CountersGraph} memoryCountersPane
    368  * @param {string} title
    369  * @param {string} currentValueLabel
    370  * @param {string} graphColor
    371  * @param {!WebInspector.CountersGraph.Counter} counter
    372  */
    373 WebInspector.CountersGraph.CounterUI = function(memoryCountersPane, title, currentValueLabel, graphColor, counter)
    374 {
    375     this._memoryCountersPane = memoryCountersPane;
    376     this.counter = counter;
    377     var container = memoryCountersPane.sidebarElement().createChild("div", "memory-counter-sidebar-info");
    378     var swatchColor = graphColor;
    379     this._swatch = new WebInspector.SwatchCheckbox(WebInspector.UIString(title), swatchColor);
    380     this._swatch.addEventListener(WebInspector.SwatchCheckbox.Events.Changed, this._toggleCounterGraph.bind(this));
    381     container.appendChild(this._swatch.element);
    382     this._range = this._swatch.element.createChild("span");
    383 
    384     this._value = memoryCountersPane._currentValuesBar.createChild("span", "memory-counter-value");
    385     this._value.style.color = graphColor;
    386     this.graphColor = graphColor;
    387     this.limitColor = WebInspector.Color.parse(graphColor).setAlpha(0.3).toString(WebInspector.Color.Format.RGBA);
    388     this.graphYValues = [];
    389     this._verticalPadding = 10;
    390 
    391     this._currentValueLabel = currentValueLabel;
    392     this._marker = memoryCountersPane._canvasContainer.createChild("div", "memory-counter-marker");
    393     this._marker.style.backgroundColor = graphColor;
    394     this._clearCurrentValueAndMarker();
    395 }
    396 
    397 WebInspector.CountersGraph.CounterUI.prototype = {
    398     reset: function()
    399     {
    400         this._range.textContent = "";
    401     },
    402 
    403     /**
    404      * @param {number} minValue
    405      * @param {number} maxValue
    406      */
    407     setRange: function(minValue, maxValue)
    408     {
    409         this._range.textContent = WebInspector.UIString("[%.0f:%.0f]", minValue, maxValue);
    410     },
    411 
    412     _toggleCounterGraph: function(event)
    413     {
    414         this._value.classList.toggle("hidden", !this._swatch.checked);
    415         this._memoryCountersPane.refresh();
    416     },
    417 
    418     /**
    419      * @param {number} x
    420      * @return {number}
    421      */
    422     _recordIndexAt: function(x)
    423     {
    424         return this.counter.x.upperBound(x * window.devicePixelRatio, null, this.counter._minimumIndex + 1, this.counter._maximumIndex + 1) - 1;
    425     },
    426 
    427     /**
    428      * @param {number} x
    429      */
    430     updateCurrentValue: function(x)
    431     {
    432         if (!this.visible() || !this.counter.values.length || !this.counter.x)
    433             return;
    434         var index = this._recordIndexAt(x);
    435         this._value.textContent = WebInspector.UIString(this._currentValueLabel, this.counter.values[index]);
    436         var y = this.graphYValues[index] / window.devicePixelRatio;
    437         this._marker.style.left = x + "px";
    438         this._marker.style.top = y + "px";
    439         this._marker.classList.remove("hidden");
    440     },
    441 
    442     _clearCurrentValueAndMarker: function()
    443     {
    444         this._value.textContent = "";
    445         this._marker.classList.add("hidden");
    446     },
    447 
    448     /**
    449      * @param {!HTMLCanvasElement} canvas
    450      */
    451     _drawGraph: function(canvas)
    452     {
    453         var ctx = canvas.getContext("2d");
    454         var width = canvas.width;
    455         var height = canvas.height - 2 * this._verticalPadding;
    456         if (height <= 0) {
    457             this.graphYValues = [];
    458             return;
    459         }
    460         var originY = this._verticalPadding;
    461         var counter = this.counter;
    462         var values = counter.values;
    463 
    464         if (!values.length)
    465             return;
    466 
    467         var bounds = counter._calculateBounds();
    468         var minValue = bounds.min;
    469         var maxValue = bounds.max;
    470         this.setRange(minValue, maxValue);
    471 
    472         if (!this.visible())
    473             return;
    474 
    475         var yValues = this.graphYValues;
    476         var maxYRange = maxValue - minValue;
    477         var yFactor = maxYRange ? height / (maxYRange) : 1;
    478 
    479         ctx.save();
    480         ctx.lineWidth = window.devicePixelRatio;
    481         if (ctx.lineWidth % 2)
    482             ctx.translate(0.5, 0.5);
    483         ctx.beginPath();
    484         var value = values[counter._minimumIndex];
    485         var currentY = Math.round(originY + height - (value - minValue) * yFactor);
    486         ctx.moveTo(0, currentY);
    487         for (var i = counter._minimumIndex; i <= counter._maximumIndex; i++) {
    488              var x = Math.round(counter.x[i]);
    489              ctx.lineTo(x, currentY);
    490              var currentValue = values[i];
    491              if (typeof currentValue !== "undefined")
    492                 value = currentValue;
    493              currentY = Math.round(originY + height - (value - minValue) * yFactor);
    494              ctx.lineTo(x, currentY);
    495              yValues[i] = currentY;
    496         }
    497         yValues.length = i;
    498         ctx.lineTo(width, currentY);
    499         ctx.strokeStyle = this.graphColor;
    500         ctx.stroke();
    501         if (counter._limitValue) {
    502             var limitLineY = Math.round(originY + height - (counter._limitValue - minValue) * yFactor);
    503             ctx.moveTo(0, limitLineY);
    504             ctx.lineTo(width, limitLineY);
    505             ctx.strokeStyle = this.limitColor;
    506             ctx.stroke();
    507         }
    508         ctx.closePath();
    509         ctx.restore();
    510     },
    511 
    512     /**
    513      * @return {boolean}
    514      */
    515     visible: function()
    516     {
    517         return this._swatch.checked;
    518     }
    519 }
    520 
    521 
    522 /**
    523  * @constructor
    524  * @extends {WebInspector.Object}
    525  */
    526 WebInspector.SwatchCheckbox = function(title, color)
    527 {
    528     this.element = document.createElement("div");
    529     this._swatch = this.element.createChild("div", "swatch");
    530     this.element.createChild("span", "title").textContent = title;
    531     this._color = color;
    532     this.checked = true;
    533 
    534     this.element.addEventListener("click", this._toggleCheckbox.bind(this), true);
    535 }
    536 
    537 WebInspector.SwatchCheckbox.Events = {
    538     Changed: "Changed"
    539 }
    540 
    541 WebInspector.SwatchCheckbox.prototype = {
    542     get checked()
    543     {
    544         return this._checked;
    545     },
    546 
    547     set checked(v)
    548     {
    549         this._checked = v;
    550         if (this._checked)
    551             this._swatch.style.backgroundColor = this._color;
    552         else
    553             this._swatch.style.backgroundColor = "";
    554     },
    555 
    556     _toggleCheckbox: function(event)
    557     {
    558         this.checked = !this.checked;
    559         this.dispatchEventToListeners(WebInspector.SwatchCheckbox.Events.Changed);
    560     },
    561 
    562     __proto__: WebInspector.Object.prototype
    563 }
    564