2009-11-22 Pavel Feldman <pfeldman@chromium.org>
[WebKit-https.git] / WebCore / inspector / front-end / TimelinePanel.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.TimelinePanel = function()
32 {
33     WebInspector.Panel.call(this);
34     this.element.addStyleClass("timeline");
35
36     this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories);
37     this._overviewPane.addEventListener("window changed", this._scheduleRefresh, this);
38     this._overviewPane.addEventListener("filter changed", this._refresh, this);
39     this.element.appendChild(this._overviewPane.element);
40
41     this._containerElement = document.createElement("div");
42     this._containerElement.id = "timeline-container";
43     this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
44     this.element.appendChild(this._containerElement);
45
46     this.createSidebar(this._containerElement, this._containerElement);
47     this.sidebarElement.id = "timeline-sidebar";
48     this.itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
49     this.itemsTreeElement.expanded = true;
50     this.sidebarTree.appendChild(this.itemsTreeElement);
51
52     this._sidebarListElement = document.createElement("div");
53     this.sidebarElement.appendChild(this._sidebarListElement);
54
55     this._containerContentElement = document.createElement("div");
56     this._containerContentElement.id = "resources-container-content";
57     this._containerElement.appendChild(this._containerContentElement);
58
59     this._timelineGrid = new WebInspector.TimelineGrid();
60     this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
61     this._itemsGraphsElement.id = "timeline-graphs";
62     this._containerContentElement.appendChild(this._timelineGrid.element);
63
64     this._topGapElement = document.createElement("div");
65     this._topGapElement.className = "timeline-gap";
66     this._itemsGraphsElement.appendChild(this._topGapElement);
67
68     this._graphRowsElement = document.createElement("div");
69     this._itemsGraphsElement.appendChild(this._graphRowsElement);
70
71     this._bottomGapElement = document.createElement("div");
72     this._bottomGapElement.className = "timeline-gap";
73     this._itemsGraphsElement.appendChild(this._bottomGapElement);
74
75     this._createStatusbarButtons();
76
77     this._records = [];
78     this._sendRequestRecords = {};
79     this._calculator = new WebInspector.TimelineCalculator();
80 }
81
82 WebInspector.TimelinePanel.prototype = {
83     toolbarItemClass: "timeline",
84
85     get toolbarItemLabel()
86     {
87         return WebInspector.UIString("Timeline");
88     },
89
90     get statusBarItems()
91     {
92         return [this.toggleTimelineButton.element, this.clearButton.element];
93     },
94
95     get categories()
96     {
97         if (!this._categories) {
98             this._categories = {
99                 loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"),
100                 scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"),
101                 rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)")
102             };
103         }
104         return this._categories;
105     },
106
107     _createStatusbarButtons: function()
108     {
109         this.toggleTimelineButton = new WebInspector.StatusBarButton("", "record-profile-status-bar-item");
110         this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false);
111
112         this.clearButton = new WebInspector.StatusBarButton("", "timeline-clear-status-bar-item");
113         this.clearButton.addEventListener("click", this.reset.bind(this), false);
114     },
115
116     _toggleTimelineButtonClicked: function()
117     {
118         if (this.toggleTimelineButton.toggled)
119             InspectorController.stopTimelineProfiler();
120         else
121             InspectorController.startTimelineProfiler();
122     },
123
124     timelineWasStarted: function()
125     {
126         this.toggleTimelineButton.toggled = true;
127     },
128
129     timelineWasStopped: function()
130     {
131         this.toggleTimelineButton.toggled = false;
132     },
133
134     addRecordToTimeline: function(record)
135     {
136         var formattedRecord = this._formatRecord(record);
137         // Glue subsequent records with same category and title together if they are closer than 100ms to each other.
138         if (this._lastRecord && (!record.children || !record.children.length) &&
139                 this._lastRecord.category == formattedRecord.category &&
140                 this._lastRecord.title == formattedRecord.title &&
141                 this._lastRecord.details == formattedRecord.details &&
142                 formattedRecord.startTime - this._lastRecord.endTime < 0.1) {
143             this._lastRecord.endTime = formattedRecord.endTime;
144             this._lastRecord.count++;
145         } else {
146             this._records.push(formattedRecord);
147
148             for (var i = 0; record.children && i < record.children.length; ++i)
149                 this.addRecordToTimeline(record.children[i]);
150             this._lastRecord = record.children && record.children.length ? null : formattedRecord;
151         }
152         this._scheduleRefresh();
153     },
154
155     _formatRecord: function(record)
156     {
157         var recordTypes = WebInspector.TimelineAgent.RecordType;
158         if (!this._recordStyles) {
159             this._recordStyles = {};
160             this._recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting };
161             this._recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering };
162             this._recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering };
163             this._recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering };
164             this._recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading };
165             this._recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting };
166             this._recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting };
167             this._recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting };
168             this._recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting };
169             this._recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting };
170             this._recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting };
171             this._recordStyles[recordTypes.MarkTimeline] = { title: WebInspector.UIString("Mark"), category: this.categories.scripting };
172             this._recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading };
173             this._recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading };
174             this._recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading };
175         }
176
177         var style = this._recordStyles[record.type];
178         if (!style)
179             style = this._recordStyles[recordTypes.EventDispatch];
180
181         var formattedRecord = {};
182         formattedRecord.category = style.category;
183         formattedRecord.title = style.title;
184         formattedRecord.startTime = record.startTime / 1000;
185         formattedRecord.data = record.data;
186         formattedRecord.count = 1;
187         formattedRecord.type = record.type;
188         formattedRecord.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : formattedRecord.startTime;
189         formattedRecord.record = record;
190
191         // Make resource receive record last since request was sent; make finish record last since response received.
192         if (record.type === WebInspector.TimelineAgent.RecordType.ResourceSendRequest) {
193             this._sendRequestRecords[record.data.identifier] = formattedRecord;
194         } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse) {
195             var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
196             sendRequestRecord._responseReceivedFormattedTime = formattedRecord.startTime;
197             formattedRecord.startTime = sendRequestRecord.startTime;
198             sendRequestRecord.details = this._getRecordDetails(record);
199         } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceFinish) {
200             var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
201             if (sendRequestRecord) // False for main resource.
202                 formattedRecord.startTime = sendRequestRecord._responseReceivedFormattedTime;
203         }
204         formattedRecord.details = this._getRecordDetails(record);
205
206         return formattedRecord;
207     },
208
209     _getRecordDetails: function(record)
210     {
211         switch (record.type) {
212         case WebInspector.TimelineAgent.RecordType.EventDispatch:
213             return record.data ? record.data.type : "";
214         case WebInspector.TimelineAgent.RecordType.Paint:
215             return record.data.width + "\u2009\u00d7\u2009" + record.data.height;
216         case WebInspector.TimelineAgent.RecordType.TimerInstall:
217         case WebInspector.TimelineAgent.RecordType.TimerRemove:
218         case WebInspector.TimelineAgent.RecordType.TimerFire:
219             return record.data.timerId;
220         case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange:
221         case WebInspector.TimelineAgent.RecordType.XHRLoad:
222         case WebInspector.TimelineAgent.RecordType.EvaluateScript:
223         case WebInspector.TimelineAgent.RecordType.ResourceSendRequest:
224             return WebInspector.displayNameForURL(record.data.url);
225         case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse:
226         case WebInspector.TimelineAgent.RecordType.ResourceFinish:
227             var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
228             return sendRequestRecord ? WebInspector.displayNameForURL(sendRequestRecord.data.url) : "";
229         case WebInspector.TimelineAgent.RecordType.MarkTimeline:
230             return record.data.message;
231         default:
232             return "";
233         }
234     },
235
236     setSidebarWidth: function(width)
237     {
238         WebInspector.Panel.prototype.setSidebarWidth.call(this, width);
239         this._overviewPane.setSidebarWidth(width);
240     },
241
242     updateMainViewWidth: function(width)
243     {
244         this._containerContentElement.style.left = width + "px";
245         this._scheduleRefresh();
246         this._overviewPane.updateMainViewWidth(width);
247     },
248
249     resize: function() {
250         this._scheduleRefresh();
251     },
252
253     reset: function()
254     {
255         this._lastRecord = null;
256         this._sendRequestRecords = {};
257         this._overviewPane.reset();
258         this._records = [];
259         this._refresh();
260     },
261
262     show: function()
263     {
264         WebInspector.Panel.prototype.show.call(this);
265
266         if (this._needsRefresh)
267             this._refresh();
268     },
269
270     _onScroll: function(event)
271     {
272         var scrollTop = this._containerElement.scrollTop;
273         var dividersTop = Math.max(0, scrollTop);
274         this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
275         this._scheduleRefresh();
276     },
277
278     _scheduleRefresh: function()
279     {
280         if (this._needsRefresh)
281             return;
282         this._needsRefresh = true;
283
284         if (this.visible && !("_refreshTimeout" in this))
285             this._refreshTimeout = setTimeout(this._refresh.bind(this), 100);
286     },
287
288     _refresh: function()
289     {
290         this._needsRefresh = false;
291         if ("_refreshTimeout" in this) {
292             clearTimeout(this._refreshTimeout);
293             delete this._refreshTimeout;
294         }
295         this._overviewPane.update(this._records);
296         this._refreshRecords();
297     },
298
299     _refreshRecords: function()
300     {
301         this._calculator.windowLeft = this._overviewPane.windowLeft;
302         this._calculator.windowRight = this._overviewPane.windowRight;
303         this._calculator.reset();
304
305         for (var i = 0; i < this._records.length; ++i)
306             this._calculator.updateBoundaries(this._records[i]);
307
308         var recordsInWindow = [];
309         for (var i = 0; i < this._records.length; ++i) {
310             var record = this._records[i];
311             var percentages = this._calculator.computeBarGraphPercentages(record);
312             if (percentages.start < 100 && percentages.end >= 0 && !record.category.hidden)
313                 recordsInWindow.push(record);
314         }
315
316         // Calculate the visible area.
317         var visibleTop = this._containerElement.scrollTop;
318         var visibleBottom = visibleTop + this._containerElement.clientHeight;
319         const rowHeight = 18;
320
321         // Convert visible area to visible indexes.
322         var startIndex = Math.max(0, Math.floor(visibleTop / rowHeight) - 1);
323         var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
324
325         var listRowElement = this._sidebarListElement.firstChild;
326         var graphRowElement = this._graphRowsElement.firstChild;
327         for (var i = startIndex; i < endIndex; ++i) {
328             var record = recordsInWindow[i];
329             var isEven = !(i % 2);
330
331             if (!listRowElement) {
332                 listRowElement = new WebInspector.TimelineRecordListRow().element;
333                 this._sidebarListElement.appendChild(listRowElement);
334             }
335             if (!graphRowElement) {
336                 graphRowElement = new WebInspector.TimelineRecordGraphRow().element;
337                 this._graphRowsElement.appendChild(graphRowElement);
338             }
339
340             listRowElement.listRow.update(record, isEven);
341             graphRowElement.graphRow.update(record, isEven, this._calculator);
342
343             listRowElement = listRowElement.nextSibling;
344             graphRowElement = graphRowElement.nextSibling;
345         }
346
347         while (listRowElement) {
348             var nextElement = listRowElement.nextSibling;
349             listRowElement.parentElement.removeChild(listRowElement);
350             listRowElement = nextElement;
351         }
352
353         while (graphRowElement) {
354             var nextElement = graphRowElement.nextSibling;
355             graphRowElement.parentElement.removeChild(graphRowElement);
356             graphRowElement = nextElement;
357         }
358
359         this._timelineGrid.updateDividers(true, this._calculator);
360
361         const top = (startIndex * rowHeight) + "px";
362         this._topGapElement.style.height = top;
363         this.sidebarElement.style.top = top;
364         this.sidebarResizeElement.style.top = top;
365         this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
366         this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight);
367     },
368
369     _adjustScrollPosition: function(totalHeight)
370     {
371         // Prevent the container from being scrolled off the end.
372         if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1)
373             this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
374     }
375 }
376
377 WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
378
379
380 WebInspector.TimelineCategory = function(name, title, color)
381 {
382     this.name = name;
383     this.title = title;
384     this.color = color;
385 }
386
387
388 WebInspector.TimelineCalculator = function()
389 {
390     this.windowLeft = 0.0;
391     this.windowRight = 1.0;
392     this._uiString = WebInspector.UIString.bind(WebInspector);
393 }
394
395 WebInspector.TimelineCalculator.prototype = {
396     computeBarGraphPercentages: function(record)
397     {
398         var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
399         var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100;
400         return {start: start, end: end};
401     },
402
403     get minimumBoundary()
404     {
405         if (typeof this._minimumBoundary === "number")
406             return this._minimumBoundary;
407
408         if (typeof this.windowLeft === "number")
409             this._minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
410         else
411             this._minimumBoundary = this._absoluteMinimumBoundary;
412         return this._minimumBoundary;
413     },
414
415     get maximumBoundary()
416     {
417         if (typeof this._maximumBoundary === "number")
418             return this._maximumBoundary;
419
420         if (typeof this.windowLeft === "number")
421             this._maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
422         else
423             this._maximumBoundary = this._absoluteMaximumBoundary;
424         return this._maximumBoundary;
425     },
426
427     reset: function()
428     {
429         delete this._absoluteMinimumBoundary;
430         delete this._absoluteMaximumBoundary;
431         delete this._minimumBoundary;
432         delete this._maximumBoundary;
433     },
434
435     updateBoundaries: function(record)
436     {
437         var didChange = false;
438
439         var lowerBound = record.startTime;
440
441         if (typeof this._absoluteMinimumBoundary === "undefined" || lowerBound < this._absoluteMinimumBoundary) {
442             this._absoluteMinimumBoundary = lowerBound;
443             delete this._minimumBoundary;
444             didChange = true;
445         }
446
447         var upperBound = record.endTime;
448         if (typeof this._absoluteMaximumBoundary === "undefined" || upperBound > this._absoluteMaximumBoundary) {
449             this._absoluteMaximumBoundary = upperBound;
450             delete this._maximumBoundary;
451             didChange = true;
452         }
453
454         return didChange;
455     },
456
457     get boundarySpan()
458     {
459         return this.maximumBoundary - this.minimumBoundary;
460     },
461
462     formatValue: function(value)
463     {
464         return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary, this._uiString);
465     }
466 }
467
468
469 WebInspector.TimelineRecordListRow = function()
470 {
471     this.element = document.createElement("div");
472     this.element.listRow = this;
473
474     var iconElement = document.createElement("span");
475     iconElement.className = "timeline-tree-icon";
476     this.element.appendChild(iconElement);
477
478     this._typeElement = document.createElement("span");
479     this._typeElement.className = "type";
480     this.element.appendChild(this._typeElement);
481
482     var separatorElement = document.createElement("span");
483     separatorElement.className = "separator";
484     separatorElement.textContent = " ";
485
486     this._dataElement = document.createElement("span");
487     this._dataElement.className = "data dimmed";
488
489     this._repeatCountElement = document.createElement("span");
490     this._repeatCountElement.className = "count";
491
492     this.element.appendChild(separatorElement);
493     this.element.appendChild(this._dataElement);
494     this.element.appendChild(this._repeatCountElement);
495 }
496
497 WebInspector.TimelineRecordListRow.prototype = {
498     update: function(record, isEven)
499     {
500         this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : "");
501         this._typeElement.textContent = record.title;
502
503         if (record.details) {
504             this._dataElement.textContent = "(" + record.details + ")";
505             this._dataElement.title = record.details;
506         } else {
507             this._dataElement.textContent = "";
508             this._dataElement.title = "";
509         }
510
511         if (record.count > 1)
512             this._repeatCountElement.textContent = "\u2009\u00d7\u2009" + record.count;
513         else
514             this._repeatCountElement.textContent = "";
515     }
516 }
517
518
519 WebInspector.TimelineRecordGraphRow = function()
520 {
521     this.element = document.createElement("div");
522     this.element.graphRow = this;
523
524     this._barAreaElement = document.createElement("div");
525     this._barAreaElement.className = "timeline-graph-bar-area";
526     this.element.appendChild(this._barAreaElement);
527
528     this._barElement = document.createElement("div");
529     this._barElement.className = "timeline-graph-bar";
530     this._barAreaElement.appendChild(this._barElement);
531 }
532
533 WebInspector.TimelineRecordGraphRow.prototype = {
534     update: function(record, isEven, calculator)
535     {
536         this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : "");
537         var percentages = calculator.computeBarGraphPercentages(record);
538         this._barElement.style.setProperty("left", percentages.start + "%");
539         this._barElement.style.setProperty("right", (100 - percentages.end) + "%");
540     }
541 }