55e24c56d007ff6c12b1004c6394acfafc05533d
[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 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, WebInspector.UIString);
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 }