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