TimelineOverviewPane.js revision cad810f21b803229eb11403f9209855525a25d57
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
31WebInspector.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
90WebInspector.TimelineOverviewPane.minSelectableSize = 12;
91
92WebInspector.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
360WebInspector.TimelineOverviewPane.prototype.__proto__ = WebInspector.Object.prototype;
361
362
363WebInspector.TimelineOverviewCalculator = function()
364{
365}
366
367WebInspector.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, WebInspector.UIString);
402    }
403}
404
405
406WebInspector.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
418WebInspector.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
447WebInspector.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
458WebInspector.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
491WebInspector.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
503WebInspector.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