Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2009 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 WebInspector.TimelineOverviewPane = function(categories)
     32 {
     33     this._categories = categories;
     34 
     35     this.statusBarFilters = document.createElement("div");
     36     this.statusBarFilters.className = "status-bar-items";
     37     for (var categoryName in this._categories) {
     38         var category = this._categories[categoryName];
     39         this.statusBarFilters.appendChild(this._createTimelineCategoryStatusBarCheckbox(category, this._onCheckboxClicked.bind(this, category)));
     40     }
     41 
     42     this._overviewGrid = new WebInspector.TimelineGrid();
     43     this._overviewGrid.element.id = "timeline-overview-grid";
     44     this._overviewGrid.itemsGraphsElement.id = "timeline-overview-timelines";
     45     this._overviewGrid.element.addEventListener("mousedown", this._dragWindow.bind(this), true);
     46 
     47     this._heapGraph = new WebInspector.HeapGraph();
     48     this._heapGraph.element.id = "timeline-overview-memory";
     49     this._overviewGrid.element.insertBefore(this._heapGraph.element, this._overviewGrid.itemsGraphsElement);
     50 
     51     this.element = this._overviewGrid.element;
     52 
     53     this._categoryGraphs = {};
     54     var i = 0;
     55     for (var category in this._categories) {
     56         var categoryGraph = new WebInspector.TimelineCategoryGraph(this._categories[category], i++ % 2);
     57         this._categoryGraphs[category] = categoryGraph;
     58         this._overviewGrid.itemsGraphsElement.appendChild(categoryGraph.graphElement);
     59     }
     60     this._overviewGrid.setScrollAndDividerTop(0, 0);
     61 
     62     this._overviewWindowElement = document.createElement("div");
     63     this._overviewWindowElement.id = "timeline-overview-window";
     64     this._overviewGrid.element.appendChild(this._overviewWindowElement);
     65 
     66     this._overviewWindowBordersElement = document.createElement("div");
     67     this._overviewWindowBordersElement.className = "timeline-overview-window-rulers";
     68     this._overviewGrid.element.appendChild(this._overviewWindowBordersElement);
     69 
     70     var overviewDividersBackground = document.createElement("div");
     71     overviewDividersBackground.className = "timeline-overview-dividers-background";
     72     this._overviewGrid.element.appendChild(overviewDividersBackground);
     73 
     74     this._leftResizeElement = document.createElement("div");
     75     this._leftResizeElement.className = "timeline-window-resizer";
     76     this._leftResizeElement.style.left = 0;
     77     this._overviewGrid.element.appendChild(this._leftResizeElement);
     78 
     79     this._rightResizeElement = document.createElement("div");
     80     this._rightResizeElement.className = "timeline-window-resizer timeline-window-resizer-right";
     81     this._rightResizeElement.style.right = 0;
     82     this._overviewGrid.element.appendChild(this._rightResizeElement);
     83 
     84     this._overviewCalculator = new WebInspector.TimelineOverviewCalculator();
     85 
     86     this.windowLeft = 0.0;
     87     this.windowRight = 1.0;
     88 }
     89 
     90 WebInspector.TimelineOverviewPane.minSelectableSize = 12;
     91 
     92 WebInspector.TimelineOverviewPane.prototype = {
     93     showTimelines: function(event) {
     94         this._heapGraph.hide();
     95         this._overviewGrid.itemsGraphsElement.removeStyleClass("hidden");
     96     },
     97 
     98     showMemoryGraph: function(records) {
     99         this._heapGraph.show();
    100         this._heapGraph.update(records);
    101         this._overviewGrid.itemsGraphsElement.addStyleClass("hidden");
    102     },
    103 
    104     _onCheckboxClicked: function (category, event) {
    105         if (event.target.checked)
    106             category.hidden = false;
    107         else
    108             category.hidden = true;
    109         this._categoryGraphs[category.name].dimmed = !event.target.checked;
    110         this.dispatchEventToListeners("filter changed");
    111     },
    112 
    113     _forAllRecords: function(recordsArray, callback)
    114     {
    115         if (!recordsArray)
    116             return;
    117         for (var i = 0; i < recordsArray.length; ++i) {
    118             callback(recordsArray[i]);
    119             this._forAllRecords(recordsArray[i].children, callback);
    120         }
    121     },
    122 
    123     update: function(records, showShortEvents)
    124     {
    125         this._showShortEvents = showShortEvents;
    126         // Clear summary bars.
    127         var timelines = {};
    128         for (var category in this._categories) {
    129             timelines[category] = [];
    130             this._categoryGraphs[category].clearChunks();
    131         }
    132 
    133         // Create sparse arrays with 101 cells each to fill with chunks for a given category.
    134         this._overviewCalculator.reset();
    135         this._forAllRecords(records, this._overviewCalculator.updateBoundaries.bind(this._overviewCalculator));
    136 
    137         function markTimeline(record)
    138         {
    139             if (!(this._showShortEvents || record.isLong()))
    140                 return;
    141             var percentages = this._overviewCalculator.computeBarGraphPercentages(record);
    142 
    143             var end = Math.round(percentages.end);
    144             var categoryName = record.category.name;
    145             for (var j = Math.round(percentages.start); j <= end; ++j)
    146                 timelines[categoryName][j] = true;
    147         }
    148         this._forAllRecords(records, markTimeline.bind(this));
    149 
    150         // Convert sparse arrays to continuous segments, render graphs for each.
    151         for (var category in this._categories) {
    152             var timeline = timelines[category];
    153             window.timelineSaved = timeline;
    154             var chunkStart = -1;
    155             for (var j = 0; j < 101; ++j) {
    156                 if (timeline[j]) {
    157                     if (chunkStart === -1)
    158                         chunkStart = j;
    159                 } else {
    160                     if (chunkStart !== -1) {
    161                         this._categoryGraphs[category].addChunk(chunkStart, j);
    162                         chunkStart = -1;
    163                     }
    164                 }
    165             }
    166             if (chunkStart !== -1) {
    167                 this._categoryGraphs[category].addChunk(chunkStart, 100);
    168                 chunkStart = -1;
    169             }
    170         }
    171 
    172         this._heapGraph.setSize(this._overviewGrid.element.offsetWidth, 60);
    173         if (this._heapGraph.visible)
    174             this._heapGraph.update(records);
    175 
    176         this._overviewGrid.updateDividers(true, this._overviewCalculator);
    177     },
    178 
    179     updateEventDividers: function(records, dividerConstructor)
    180     {
    181         this._overviewGrid.removeEventDividers();
    182         var dividers = [];
    183         for (var i = 0; i < records.length; ++i) {
    184             var record = records[i];
    185             var positions = this._overviewCalculator.computeBarGraphPercentages(record);
    186             var dividerPosition = Math.round(positions.start * 10);
    187             if (dividers[dividerPosition])
    188                 continue;
    189             var divider = dividerConstructor(record);
    190             divider.style.left = positions.start + "%";
    191             dividers[dividerPosition] = divider;
    192         }
    193         this._overviewGrid.addEventDividers(dividers);
    194     },
    195 
    196     updateMainViewWidth: function(width, records)
    197     {
    198         this._overviewGrid.element.style.left = width + "px";
    199         this.statusBarFilters.style.left = Math.max(155, width) + "px";
    200     },
    201 
    202     reset: function()
    203     {
    204         this.windowLeft = 0.0;
    205         this.windowRight = 1.0;
    206         this._overviewWindowElement.style.left = "0%";
    207         this._overviewWindowElement.style.width = "100%";
    208         this._overviewWindowBordersElement.style.left = "0%";
    209         this._overviewWindowBordersElement.style.right = "0%";
    210         this._leftResizeElement.style.left = "0%";
    211         this._rightResizeElement.style.left = "100%";
    212         this._overviewCalculator.reset();
    213         this._overviewGrid.updateDividers(true, this._overviewCalculator);
    214     },
    215 
    216     _resizeWindow: function(resizeElement, event)
    217     {
    218         WebInspector.elementDragStart(resizeElement, this._windowResizeDragging.bind(this, resizeElement), this._endWindowDragging.bind(this), event, "col-resize");
    219     },
    220 
    221     _windowResizeDragging: function(resizeElement, event)
    222     {
    223         if (resizeElement === this._leftResizeElement)
    224             this._resizeWindowLeft(event.pageX - this._overviewGrid.element.offsetLeft);
    225         else
    226             this._resizeWindowRight(event.pageX - this._overviewGrid.element.offsetLeft);
    227         event.preventDefault();
    228     },
    229 
    230     _dragWindow: function(event)
    231     {
    232         var node = event.target;
    233         while (node) {
    234             if (node === this._overviewGrid._dividersLabelBarElement) {
    235                 WebInspector.elementDragStart(this._overviewWindowElement, this._windowDragging.bind(this, event.pageX,
    236                     this._leftResizeElement.offsetLeft, this._rightResizeElement.offsetLeft), this._endWindowDragging.bind(this), event, "ew-resize");
    237                 break;
    238             } else if (node === this._overviewGrid.element) {
    239                 var position = event.pageX - this._overviewGrid.element.offsetLeft;
    240                 this._overviewWindowSelector = new WebInspector.TimelinePanel.WindowSelector(this._overviewGrid.element, position, event);
    241                 WebInspector.elementDragStart(null, this._windowSelectorDragging.bind(this), this._endWindowSelectorDragging.bind(this), event, "col-resize");
    242                 break;
    243             } else if (node === this._leftResizeElement || node === this._rightResizeElement) {
    244                 this._resizeWindow(node, event);
    245                 break;
    246             }
    247             node = node.parentNode;
    248         }
    249     },
    250 
    251     _windowSelectorDragging: function(event)
    252     {
    253         this._overviewWindowSelector._updatePosition(event.pageX - this._overviewGrid.element.offsetLeft);
    254         event.preventDefault();
    255     },
    256 
    257     _endWindowSelectorDragging: function(event)
    258     {
    259         WebInspector.elementDragEnd(event);
    260         var window = this._overviewWindowSelector._close(event.pageX - this._overviewGrid.element.offsetLeft);
    261         delete this._overviewWindowSelector;
    262         if (window.end - window.start < WebInspector.TimelineOverviewPane.minSelectableSize)
    263             if (this._overviewGrid.itemsGraphsElement.offsetWidth - window.end > WebInspector.TimelineOverviewPane.minSelectableSize)
    264                 window.end = window.start + WebInspector.TimelineOverviewPane.minSelectableSize;
    265             else
    266                 window.start = window.end - WebInspector.TimelineOverviewPane.minSelectableSize;
    267         this._setWindowPosition(window.start, window.end);
    268     },
    269 
    270     _windowDragging: function(startX, windowLeft, windowRight, event)
    271     {
    272         var delta = event.pageX - startX;
    273         var start = windowLeft + delta;
    274         var end = windowRight + delta;
    275         var windowSize = windowRight - windowLeft;
    276 
    277         if (start < 0) {
    278             start = 0;
    279             end = windowSize;
    280         }
    281 
    282         if (end > this._overviewGrid.element.clientWidth) {
    283             end = this._overviewGrid.element.clientWidth;
    284             start = end - windowSize;
    285         }
    286         this._setWindowPosition(start, end);
    287 
    288         event.preventDefault();
    289     },
    290 
    291     _resizeWindowLeft: function(start)
    292     {
    293         // Glue to edge.
    294         if (start < 10)
    295             start = 0;
    296         else if (start > this._rightResizeElement.offsetLeft -  4)
    297             start = this._rightResizeElement.offsetLeft - 4;
    298         this._setWindowPosition(start, null);
    299     },
    300 
    301     _resizeWindowRight: function(end)
    302     {
    303         // Glue to edge.
    304         if (end > this._overviewGrid.element.clientWidth - 10)
    305             end = this._overviewGrid.element.clientWidth;
    306         else if (end < this._leftResizeElement.offsetLeft + WebInspector.TimelineOverviewPane.minSelectableSize)
    307             end = this._leftResizeElement.offsetLeft + WebInspector.TimelineOverviewPane.minSelectableSize;
    308         this._setWindowPosition(null, end);
    309     },
    310 
    311     _setWindowPosition: function(start, end)
    312     {
    313         const rulerAdjustment = 1 / this._overviewGrid.element.clientWidth;
    314         if (typeof start === "number") {
    315             this.windowLeft = start / this._overviewGrid.element.clientWidth;
    316             this._leftResizeElement.style.left = this.windowLeft * 100 + "%";
    317             this._overviewWindowElement.style.left = this.windowLeft * 100 + "%";
    318             this._overviewWindowBordersElement.style.left = (this.windowLeft - rulerAdjustment) * 100 + "%";
    319         }
    320         if (typeof end === "number") {
    321             this.windowRight = end / this._overviewGrid.element.clientWidth;
    322             this._rightResizeElement.style.left = this.windowRight * 100 + "%";
    323         }
    324         this._overviewWindowElement.style.width = (this.windowRight - this.windowLeft) * 100 + "%";
    325         this._overviewWindowBordersElement.style.right = (1 - this.windowRight + 2 * rulerAdjustment) * 100 + "%";
    326         this.dispatchEventToListeners("window changed");
    327     },
    328 
    329     _endWindowDragging: function(event)
    330     {
    331         WebInspector.elementDragEnd(event);
    332     },
    333 
    334     _createTimelineCategoryStatusBarCheckbox: function(category, onCheckboxClicked)
    335     {
    336         var labelContainer = document.createElement("div");
    337         labelContainer.addStyleClass("timeline-category-statusbar-item");
    338         labelContainer.addStyleClass("timeline-category-" + category.name);
    339         labelContainer.addStyleClass("status-bar-item");
    340 
    341         var label = document.createElement("label");
    342         var checkElement = document.createElement("input");
    343         checkElement.type = "checkbox";
    344         checkElement.className = "timeline-category-checkbox";
    345         checkElement.checked = true;
    346         checkElement.addEventListener("click", onCheckboxClicked);
    347         label.appendChild(checkElement);
    348 
    349         var typeElement = document.createElement("span");
    350         typeElement.className = "type";
    351         typeElement.textContent = category.title;
    352         label.appendChild(typeElement);
    353 
    354         labelContainer.appendChild(label);
    355         return labelContainer;
    356     }
    357 
    358 }
    359 
    360 WebInspector.TimelineOverviewPane.prototype.__proto__ = WebInspector.Object.prototype;
    361 
    362 
    363 WebInspector.TimelineOverviewCalculator = function()
    364 {
    365 }
    366 
    367 WebInspector.TimelineOverviewCalculator.prototype = {
    368     computeBarGraphPercentages: function(record)
    369     {
    370         var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
    371         var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100;
    372         return {start: start, end: end};
    373     },
    374 
    375     reset: function()
    376     {
    377         delete this.minimumBoundary;
    378         delete this.maximumBoundary;
    379     },
    380 
    381     updateBoundaries: function(record)
    382     {
    383         if (typeof this.minimumBoundary === "undefined" || record.startTime < this.minimumBoundary) {
    384             this.minimumBoundary = record.startTime;
    385             return true;
    386         }
    387         if (typeof this.maximumBoundary === "undefined" || record.endTime > this.maximumBoundary) {
    388             this.maximumBoundary = record.endTime;
    389             return true;
    390         }
    391         return false;
    392     },
    393 
    394     get boundarySpan()
    395     {
    396         return this.maximumBoundary - this.minimumBoundary;
    397     },
    398 
    399     formatValue: function(value)
    400     {
    401         return Number.secondsToString(value);
    402     }
    403 }
    404 
    405 
    406 WebInspector.TimelineCategoryGraph = function(category, isEven)
    407 {
    408     this._category = category;
    409 
    410     this._graphElement = document.createElement("div");
    411     this._graphElement.className = "timeline-graph-side timeline-overview-graph-side" + (isEven ? " even" : "");
    412 
    413     this._barAreaElement = document.createElement("div");
    414     this._barAreaElement.className = "timeline-graph-bar-area timeline-category-" + category.name;
    415     this._graphElement.appendChild(this._barAreaElement);
    416 }
    417 
    418 WebInspector.TimelineCategoryGraph.prototype = {
    419     get graphElement()
    420     {
    421         return this._graphElement;
    422     },
    423 
    424     addChunk: function(start, end)
    425     {
    426         var chunk = document.createElement("div");
    427         chunk.className = "timeline-graph-bar";
    428         this._barAreaElement.appendChild(chunk);
    429         chunk.style.setProperty("left", start + "%");
    430         chunk.style.setProperty("width", (end - start) + "%");
    431     },
    432 
    433     clearChunks: function()
    434     {
    435         this._barAreaElement.removeChildren();
    436     },
    437 
    438     set dimmed(dimmed)
    439     {
    440         if (dimmed)
    441             this._barAreaElement.removeStyleClass("timeline-category-" + this._category.name);
    442         else
    443             this._barAreaElement.addStyleClass("timeline-category-" + this._category.name);
    444     }
    445 }
    446 
    447 WebInspector.TimelinePanel.WindowSelector = function(parent, position, event)
    448 {
    449     this._startPosition = position;
    450     this._width = parent.offsetWidth;
    451     this._windowSelector = document.createElement("div");
    452     this._windowSelector.className = "timeline-window-selector";
    453     this._windowSelector.style.left = this._startPosition + "px";
    454     this._windowSelector.style.right = this._width - this._startPosition +  + "px";
    455     parent.appendChild(this._windowSelector);
    456 }
    457 
    458 WebInspector.TimelinePanel.WindowSelector.prototype = {
    459     _createSelectorElement: function(parent, left, width, height)
    460     {
    461         var selectorElement = document.createElement("div");
    462         selectorElement.className = "timeline-window-selector";
    463         selectorElement.style.left = left + "px";
    464         selectorElement.style.width = width + "px";
    465         selectorElement.style.top = "0px";
    466         selectorElement.style.height = height + "px";
    467         parent.appendChild(selectorElement);
    468         return selectorElement;
    469     },
    470 
    471     _close: function(position)
    472     {
    473         position = Math.max(0, Math.min(position, this._width));
    474         this._windowSelector.parentNode.removeChild(this._windowSelector);
    475         return this._startPosition < position ? {start: this._startPosition, end: position} : {start: position, end: this._startPosition};
    476     },
    477 
    478     _updatePosition: function(position)
    479     {
    480         position = Math.max(0, Math.min(position, this._width));
    481         if (position < this._startPosition) {
    482             this._windowSelector.style.left = position + "px";
    483             this._windowSelector.style.right = this._width - this._startPosition + "px";
    484         } else {
    485             this._windowSelector.style.left = this._startPosition + "px";
    486             this._windowSelector.style.right = this._width - position + "px";
    487         }
    488     }
    489 }
    490 
    491 WebInspector.HeapGraph = function() {
    492     this._canvas = document.createElement("canvas");
    493 
    494     this._maxHeapSizeLabel = document.createElement("div");
    495     this._maxHeapSizeLabel.addStyleClass("memory-graph-label");
    496 
    497     this._element = document.createElement("div");
    498     this._element.addStyleClass("hidden");
    499     this._element.appendChild(this._canvas);
    500     this._element.appendChild(this._maxHeapSizeLabel);
    501 }
    502 
    503 WebInspector.HeapGraph.prototype = {
    504     get element() {
    505     //    return this._canvas;
    506         return this._element;
    507     },
    508 
    509     get visible() {
    510         return !this.element.hasStyleClass("hidden");
    511     },
    512 
    513     show: function() {
    514         this.element.removeStyleClass("hidden");
    515     },
    516 
    517     hide: function() {
    518         this.element.addStyleClass("hidden");
    519     },
    520 
    521     setSize: function(w, h) {
    522         this._canvas.width = w;
    523         this._canvas.height = h - 5;
    524     },
    525 
    526     update: function(records)
    527     {
    528         if (!records.length)
    529             return;
    530 
    531         var maxTotalHeapSize = 0;
    532         var minTime;
    533         var maxTime;
    534         this._forAllRecords(records, function(r) {
    535             if (r.totalHeapSize && r.totalHeapSize > maxTotalHeapSize)
    536                 maxTotalHeapSize = r.totalHeapSize;
    537 
    538             if (typeof minTime === "undefined" || r.startTime < minTime)
    539                 minTime = r.startTime;
    540             if (typeof maxTime === "undefined" || r.endTime > maxTime)
    541                 maxTime = r.endTime;
    542         });
    543 
    544         var width = this._canvas.width;
    545         var height = this._canvas.height;
    546         var xFactor = width / (maxTime - minTime);
    547         var yFactor = height / maxTotalHeapSize;
    548 
    549         var histogram = new Array(width);
    550         this._forAllRecords(records, function(r) {
    551             if (!r.usedHeapSize)
    552                 return;
    553              var x = Math.round((r.endTime - minTime) * xFactor);
    554              var y = Math.round(r.usedHeapSize * yFactor);
    555              histogram[x] = Math.max(histogram[x] || 0, y);
    556         });
    557 
    558         var ctx = this._canvas.getContext("2d");
    559         this._clear(ctx);
    560 
    561         // +1 so that the border always fit into the canvas area.
    562         height = height + 1;
    563 
    564         ctx.beginPath();
    565         var initialY = 0;
    566         for (var k = 0; k < histogram.length; k++) {
    567             if (histogram[k]) {
    568                 initialY = histogram[k];
    569                 break;
    570             }
    571         }
    572         ctx.moveTo(0, height - initialY);
    573 
    574         for (var x = 0; x < histogram.length; x++) {
    575              if (!histogram[x])
    576                  continue;
    577              ctx.lineTo(x, height - histogram[x]);
    578         }
    579 
    580         ctx.lineWidth = 0.5;
    581         ctx.strokeStyle = "rgba(20,0,0,0.8)";
    582         ctx.stroke();
    583 
    584         ctx.fillStyle = "rgba(214,225,254, 0.8);";
    585         ctx.lineTo(width, 60);
    586         ctx.lineTo(0, 60);
    587         ctx.lineTo(0, height - initialY);
    588         ctx.fill();
    589         ctx.closePath();
    590 
    591         this._maxHeapSizeLabel.textContent = Number.bytesToString(maxTotalHeapSize);
    592     },
    593 
    594     _clear: function(ctx) {
    595         ctx.fillStyle = "rgba(255,255,255,0.8)";
    596         ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
    597     },
    598 
    599     _forAllRecords: WebInspector.TimelineOverviewPane.prototype._forAllRecords
    600 }
    601