2011-09-18 Ilya Tikhonovsky <loislo@chromium.org>
[WebKit-https.git] / Source / WebCore / inspector / front-end / TimelinePanel.js
1 /*
2  * Copyright (C) 2011 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, "timeline");
34
35     this.element.appendChild(this._createTopPane());
36     this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
37     this.element.tabIndex = 0;
38
39     this._sidebarBackgroundElement = document.createElement("div");
40     this._sidebarBackgroundElement.className = "sidebar timeline-sidebar-background";
41     this.element.appendChild(this._sidebarBackgroundElement);
42
43     this._containerElement = document.createElement("div");
44     this._containerElement.id = "timeline-container";
45     this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
46     this.element.appendChild(this._containerElement);
47
48     this.createSidebar(this._containerElement, this._containerElement);
49     var itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
50     itemsTreeElement.expanded = true;
51     this.sidebarTree.appendChild(itemsTreeElement);
52
53     this._sidebarListElement = document.createElement("div");
54     this.sidebarElement.appendChild(this._sidebarListElement);
55
56     this._containerContentElement = document.createElement("div");
57     this._containerContentElement.id = "resources-container-content";
58     this._containerElement.appendChild(this._containerContentElement);
59
60     this._timelineGrid = new WebInspector.TimelineGrid();
61     this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
62     this._itemsGraphsElement.id = "timeline-graphs";
63     this._itemsGraphsElement.addEventListener("mousewheel", this._overviewPane.scrollWindow.bind(this._overviewPane), true);
64     this._containerContentElement.appendChild(this._timelineGrid.element);
65
66     this._topGapElement = document.createElement("div");
67     this._topGapElement.className = "timeline-gap";
68     this._itemsGraphsElement.appendChild(this._topGapElement);
69
70     this._graphRowsElement = document.createElement("div");
71     this._itemsGraphsElement.appendChild(this._graphRowsElement);
72
73     this._bottomGapElement = document.createElement("div");
74     this._bottomGapElement.className = "timeline-gap";
75     this._itemsGraphsElement.appendChild(this._bottomGapElement);
76
77     this._expandElements = document.createElement("div");
78     this._expandElements.id = "orphan-expand-elements";
79     this._itemsGraphsElement.appendChild(this._expandElements);
80
81     this._rootRecord = this._createRootRecord();
82     this._sendRequestRecords = {};
83     this._scheduledResourceRequests = {};
84     this._timerRecords = {};
85     this._registeredAnimationCallbackRecords = {};
86
87     this._calculator = new WebInspector.TimelineCalculator();
88     this._calculator._showShortEvents = false;
89     var shortRecordThresholdTitle = Number.secondsToString(WebInspector.TimelinePanel.shortRecordThreshold);
90     this._showShortRecordsTitleText = WebInspector.UIString("Show the records that are shorter than %s", shortRecordThresholdTitle);
91     this._hideShortRecordsTitleText = WebInspector.UIString("Hide the records that are shorter than %s", shortRecordThresholdTitle);
92     this._createStatusbarButtons();
93
94     this._boundariesAreValid = true;
95     this._scrollTop = 0;
96
97     this._popoverHelper = new WebInspector.PopoverHelper(this._containerElement, this._getPopoverAnchor.bind(this), this._showPopover.bind(this));
98     this._containerElement.addEventListener("mousemove", this._mouseMove.bind(this), false);
99     this._containerElement.addEventListener("mouseout", this._mouseOut.bind(this), false);
100
101     // Disable short events filter by default.
102     this.toggleFilterButton.toggled = true;
103     this._calculator._showShortEvents = this.toggleFilterButton.toggled;
104     this._timeStampRecords = [];
105     this._expandOffset = 15;
106
107     this._createFileSelector();
108     this._model = new WebInspector.TimelineModel(this);
109
110     this._registerShortcuts();
111     WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineEventRecorded, this._onTimelineEventRecorded, this);
112 }
113
114 // Define row height, should be in sync with styles for timeline graphs.
115 WebInspector.TimelinePanel.rowHeight = 18;
116 WebInspector.TimelinePanel.shortRecordThreshold = 0.015;
117
118 WebInspector.TimelinePanel.prototype = {
119     _createTopPane: function() {
120         var topPaneElement = document.createElement("div");
121         topPaneElement.id = "timeline-overview-panel";
122
123         this._topPaneSidebarElement = document.createElement("div");
124         this._topPaneSidebarElement.id = "timeline-overview-sidebar";
125
126         var overviewTreeElement = document.createElement("ol");
127         overviewTreeElement.className = "sidebar-tree";
128         this._topPaneSidebarElement.appendChild(overviewTreeElement);
129         topPaneElement.appendChild(this._topPaneSidebarElement);
130
131         var topPaneSidebarTree = new TreeOutline(overviewTreeElement);
132         var timelinesOverviewItem = new WebInspector.SidebarTreeElement("resources-time-graph-sidebar-item", WebInspector.UIString("Timelines"));
133         topPaneSidebarTree.appendChild(timelinesOverviewItem);
134         timelinesOverviewItem.revealAndSelect(false);
135         timelinesOverviewItem.onselect = this._timelinesOverviewItemSelected.bind(this);
136
137         var memoryOverviewItem = new WebInspector.SidebarTreeElement("resources-size-graph-sidebar-item", WebInspector.UIString("Memory"));
138         topPaneSidebarTree.appendChild(memoryOverviewItem);
139         memoryOverviewItem.onselect = this._memoryOverviewItemSelected.bind(this);
140
141         this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories);
142         this._overviewPane.addEventListener("window changed", this._windowChanged, this);
143         this._overviewPane.addEventListener("filter changed", this._refresh, this);
144         topPaneElement.appendChild(this._overviewPane.element);
145
146         var separatorElement = document.createElement("div");
147         separatorElement.id = "timeline-overview-separator";
148         topPaneElement.appendChild(separatorElement);
149         return topPaneElement;
150     },
151
152     get toolbarItemLabel()
153     {
154         return WebInspector.UIString("Timeline");
155     },
156
157     get statusBarItems()
158     {
159         return [this.toggleFilterButton.element, this.toggleTimelineButton.element, this.garbageCollectButton.element, this.clearButton.element, this._overviewPane.statusBarFilters];
160     },
161
162     get categories()
163     {
164         if (!this._categories) {
165             this._categories = {
166                 loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"),
167                 scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"),
168                 rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)")
169             };
170         }
171         return this._categories;
172     },
173
174     get defaultFocusedElement()
175     {
176         return this.element;
177     },
178
179     get _recordStyles()
180     {
181         if (!this._recordStylesArray) {
182             var recordTypes = WebInspector.TimelineAgent.RecordType;
183             var recordStyles = {};
184             recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting };
185             recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering };
186             recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering };
187             recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering };
188             recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading };
189             recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting };
190             recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting };
191             recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting };
192             recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting };
193             recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting };
194             recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting };
195             recordStyles[recordTypes.TimeStamp] = { title: WebInspector.UIString("Stamp"), category: this.categories.scripting };
196             recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading };
197             recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading };
198             recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading };
199             recordStyles[recordTypes.FunctionCall] = { title: WebInspector.UIString("Function Call"), category: this.categories.scripting };
200             recordStyles[recordTypes.ResourceReceivedData] = { title: WebInspector.UIString("Receive Data"), category: this.categories.loading };
201             recordStyles[recordTypes.GCEvent] = { title: WebInspector.UIString("GC Event"), category: this.categories.scripting };
202             recordStyles[recordTypes.MarkDOMContent] = { title: WebInspector.UIString("DOMContent event"), category: this.categories.scripting };
203             recordStyles[recordTypes.MarkLoad] = { title: WebInspector.UIString("Load event"), category: this.categories.scripting };
204             recordStyles[recordTypes.ScheduleResourceRequest] = { title: WebInspector.UIString("Schedule Request"), category: this.categories.loading };
205             recordStyles[recordTypes.RegisterAnimationFrameCallback] = { title: WebInspector.UIString("Register Animation Callback"), category: this.categories.scripting };
206             recordStyles[recordTypes.CancelAnimationFrameCallback] = { title: WebInspector.UIString("Cancel Animation Callback"), category: this.categories.scripting };
207             recordStyles[recordTypes.FireAnimationFrameEvent] = { title: WebInspector.UIString("Animation Frame Event"), category: this.categories.scripting };
208             this._recordStylesArray = recordStyles;
209         }
210         return this._recordStylesArray;
211     },
212
213     _createStatusbarButtons: function()
214     {
215         this.toggleTimelineButton = new WebInspector.StatusBarButton(WebInspector.UIString("Record"), "record-profile-status-bar-item");
216         this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false);
217
218         this.clearButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear"), "clear-status-bar-item");
219         this.clearButton.addEventListener("click", this._clearPanel.bind(this), false);
220
221         this.toggleFilterButton = new WebInspector.StatusBarButton(this._hideShortRecordsTitleText, "timeline-filter-status-bar-item");
222         this.toggleFilterButton.addEventListener("click", this._toggleFilterButtonClicked.bind(this), false);
223
224         this.garbageCollectButton = new WebInspector.StatusBarButton(WebInspector.UIString("Collect Garbage"), "garbage-collect-status-bar-item");
225         this.garbageCollectButton.addEventListener("click", this._garbageCollectButtonClicked.bind(this), false);
226
227         this.recordsCounter = document.createElement("span");
228         this.recordsCounter.className = "timeline-records-counter";
229     },
230
231     _registerShortcuts: function()
232     {
233         var shortcut = WebInspector.KeyboardShortcut;
234         var modifiers = shortcut.Modifiers;
235         var section = WebInspector.shortcutsScreen.section(WebInspector.UIString("Timeline Panel"));
236
237         this._shortcuts[shortcut.makeKey("e", modifiers.CtrlOrMeta)] = this._toggleTimelineButtonClicked.bind(this);
238         section.addKey(shortcut.shortcutToString("e", modifiers.CtrlOrMeta), WebInspector.UIString("Start/stop recording"));
239
240         var shortRecordThresholdTitle = Number.secondsToString(WebInspector.TimelinePanel.shortRecordThreshold);
241         this._shortcuts[shortcut.makeKey("f", modifiers.Shift | modifiers.CtrlOrMeta)] = this._toggleFilterButtonClicked.bind(this);
242         section.addKey(shortcut.shortcutToString("f", modifiers.Shift | modifiers.CtrlOrMeta), WebInspector.UIString("Toggle filter for the records that are shorter than %s", shortRecordThresholdTitle));
243
244         this._shortcuts[shortcut.makeKey("s", modifiers.CtrlOrMeta)] = this._saveToFile.bind(this);
245         section.addKey(shortcut.shortcutToString("s", modifiers.CtrlOrMeta), WebInspector.UIString("Save Timeline data\u2026"));
246
247         this._shortcuts[shortcut.makeKey("o", modifiers.CtrlOrMeta)] = this._fileSelectorElement.click.bind(this._fileSelectorElement);
248         section.addKey(shortcut.shortcutToString("o", modifiers.CtrlOrMeta), WebInspector.UIString("Load Timeline data\u2026"));
249     },
250
251     _createFileSelector: function()
252     {
253         if (this._fileSelectorElement)
254             this.element.removeChild(this._fileSelectorElement);
255
256         var fileSelectorElement = document.createElement("input");
257         fileSelectorElement.type = "file";
258         fileSelectorElement.style.display = "none";
259         fileSelectorElement.onchange = this._loadFromFile.bind(this);
260         this.element.appendChild(fileSelectorElement);
261         this._fileSelectorElement = fileSelectorElement;
262     },
263
264     _contextMenu: function(event)
265     {
266         var contextMenu = new WebInspector.ContextMenu();
267         contextMenu.appendItem(WebInspector.UIString("&Save Timeline data\u2026"), this._saveToFile.bind(this));
268         contextMenu.appendItem(WebInspector.UIString("L&oad Timeline data\u2026"), this._fileSelectorElement.click.bind(this._fileSelectorElement));
269         contextMenu.show(event);
270     },
271
272     _saveToFile: function()
273     {
274         this._model._saveToFile();
275     },
276
277     _loadFromFile: function()
278     {
279         if (this.toggleTimelineButton.toggled)
280             WebInspector.timelineManager.stop();
281
282         this._clearPanel();
283
284         this._model._loadFromFile(this._fileSelectorElement.files[0]);
285         this._createFileSelector();
286     },
287
288     _updateRecordsCounter: function()
289     {
290         this.recordsCounter.textContent = WebInspector.UIString("%d of %d captured records are visible", this._rootRecord._visibleRecordsCount, this._rootRecord._allRecordsCount);
291     },
292
293     _updateEventDividers: function()
294     {
295         this._timelineGrid.removeEventDividers();
296         var clientWidth = this._graphRowsElement.offsetWidth - this._expandOffset;
297         var dividers = [];
298         for (var i = 0; i < this._timeStampRecords.length; ++i) {
299             var record = this._timeStampRecords[i];
300             var positions = this._calculator.computeBarGraphWindowPosition(record, clientWidth);
301             var dividerPosition = Math.round(positions.left);
302             if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition])
303                 continue;
304             var divider = this._createEventDivider(record);
305             divider.style.left = (dividerPosition + this._expandOffset) + "px";
306             dividers[dividerPosition] = divider;
307         }
308         this._timelineGrid.addEventDividers(dividers);
309         this._overviewPane.updateEventDividers(this._timeStampRecords, this._createEventDivider.bind(this));
310     },
311
312     _createEventDivider: function(record)
313     {
314         var eventDivider = document.createElement("div");
315         eventDivider.className = "resources-event-divider";
316         var recordTypes = WebInspector.TimelineAgent.RecordType;
317
318         var eventDividerPadding = document.createElement("div");
319         eventDividerPadding.className = "resources-event-divider-padding";
320         eventDividerPadding.title = record.title;
321
322         if (record.type === recordTypes.MarkDOMContent)
323             eventDivider.className += " resources-blue-divider";
324         else if (record.type === recordTypes.MarkLoad)
325             eventDivider.className += " resources-red-divider";
326         else if (record.type === recordTypes.TimeStamp) {
327             eventDivider.className += " resources-orange-divider";
328             eventDividerPadding.title = record.data.message;
329         }
330         eventDividerPadding.appendChild(eventDivider);
331         return eventDividerPadding;
332     },
333
334     _timelinesOverviewItemSelected: function(event)
335     {
336         this._overviewPane.showTimelines();
337     },
338
339     _memoryOverviewItemSelected: function(event)
340     {
341         this._overviewPane.showMemoryGraph(this._rootRecord.children);
342     },
343
344     _toggleTimelineButtonClicked: function()
345     {
346         if (this.toggleTimelineButton.toggled)
347             WebInspector.timelineManager.stop();
348         else {
349             this._clearPanel();
350             WebInspector.timelineManager.start();
351             WebInspector.userMetrics.TimelineStarted.record();
352         }
353         this.toggleTimelineButton.toggled = !this.toggleTimelineButton.toggled;
354     },
355
356     _toggleFilterButtonClicked: function()
357     {
358         this.toggleFilterButton.toggled = !this.toggleFilterButton.toggled;
359         this._calculator._showShortEvents = this.toggleFilterButton.toggled;
360         this.toggleFilterButton.element.title = this._calculator._showShortEvents ? this._hideShortRecordsTitleText : this._showShortRecordsTitleText;
361         this._scheduleRefresh(true);
362     },
363
364     _garbageCollectButtonClicked: function()
365     {
366         ProfilerAgent.collectGarbage();
367     },
368
369     _onTimelineEventRecorded: function(event)
370     {
371         if (this.toggleTimelineButton.toggled)
372             this._addRecordToTimeline(event.data);
373     },
374
375     _addRecordToTimeline: function(record)
376     {
377         if (record.type === WebInspector.TimelineAgent.RecordType.ResourceSendRequest) {
378             var isMainResource = (record.data.requestId === WebInspector.mainResource.requestId);
379             if (isMainResource && this._mainRequestId !== record.data.requestId) {
380                 // We are loading new main resource -> clear the panel. Check above is necessary since
381                 // there may be several resource loads with main resource marker upon redirects, redirects are reported with
382                 // the original request id.
383                 this._mainRequestId = record.data.requestId;
384                 this._clearPanel();
385             }
386         }
387         this._model._addRecord(record);
388         this._innerAddRecordToTimeline(record, this._rootRecord);
389         this._scheduleRefresh();
390     },
391
392     _findParentRecord: function(record)
393     {
394         var recordTypes = WebInspector.TimelineAgent.RecordType;
395         var parentRecord;
396         if (record.type === recordTypes.ResourceReceiveResponse ||
397             record.type === recordTypes.ResourceFinish ||
398             record.type === recordTypes.ResourceReceivedData)
399             parentRecord = this._sendRequestRecords[record.data.requestId];
400         else if (record.type === recordTypes.TimerFire)
401             parentRecord = this._timerRecords[record.data.timerId];
402         else if (record.type === recordTypes.ResourceSendRequest)
403             parentRecord = this._scheduledResourceRequests[record.data.url];
404         return parentRecord;
405     },
406
407     _innerAddRecordToTimeline: function(record, parentRecord)
408     {
409         var connectedToOldRecord = false;
410         var recordTypes = WebInspector.TimelineAgent.RecordType;
411
412         if (record.type === recordTypes.RegisterAnimationFrameCallback) {
413             this._registeredAnimationCallbackRecords[record.data.id] = record;
414             return;
415         }
416
417         if (record.type === recordTypes.MarkDOMContent || record.type === recordTypes.MarkLoad)
418             parentRecord = null; // No bar entry for load events.
419         else if (parentRecord === this._rootRecord ||
420                  record.type === recordTypes.ResourceReceiveResponse ||
421                  record.type === recordTypes.ResourceFinish ||
422                  record.type === recordTypes.ResourceReceivedData) {
423             var newParentRecord = this._findParentRecord(record);
424             if (newParentRecord) {
425                 parentRecord = newParentRecord;
426                 connectedToOldRecord = true;
427             }
428         }
429
430         var children = record.children;
431         var scriptDetails;
432         if (record.data && record.data.scriptName) {
433             scriptDetails = {
434                 scriptName: record.data.scriptName,
435                 scriptLine: record.data.scriptLine
436             }
437         };
438
439         if ((record.type === recordTypes.TimerFire || record.type === recordTypes.FireAnimationFrameEvent) && children && children.length) {
440             var childRecord = children[0];
441             if (childRecord.type === recordTypes.FunctionCall) {
442                 scriptDetails = {
443                     scriptName: childRecord.data.scriptName,
444                     scriptLine: childRecord.data.scriptLine
445                 };
446                 children = childRecord.children.concat(children.slice(1));
447             }
448         }
449
450         var formattedRecord = new WebInspector.TimelinePanel.FormattedRecord(record, parentRecord, this, scriptDetails);
451
452         if (record.type === recordTypes.MarkDOMContent || record.type === recordTypes.MarkLoad) {
453             this._timeStampRecords.push(formattedRecord);
454             return;
455         }
456
457         ++this._rootRecord._allRecordsCount;
458         formattedRecord.collapsed = (parentRecord === this._rootRecord);
459
460         var childrenCount = children ? children.length : 0;
461         for (var i = 0; i < childrenCount; ++i)
462             this._innerAddRecordToTimeline(children[i], formattedRecord);
463
464         formattedRecord._calculateAggregatedStats(this.categories);
465
466         if (connectedToOldRecord) {
467             var record = formattedRecord;
468             do {
469                 var parent = record.parent;
470                 parent._cpuTime += formattedRecord._cpuTime;
471                 if (parent._lastChildEndTime < record._lastChildEndTime)
472                     parent._lastChildEndTime = record._lastChildEndTime;
473                 for (var category in formattedRecord._aggregatedStats)
474                     parent._aggregatedStats[category] += formattedRecord._aggregatedStats[category];
475                 record = parent;
476             } while (record.parent);
477         } else
478             if (parentRecord !== this._rootRecord)
479                 parentRecord._selfTime -= formattedRecord.endTime - formattedRecord.startTime;
480
481         // Keep bar entry for mark timeline since nesting might be interesting to the user.
482         if (record.type === recordTypes.TimeStamp)
483             this._timeStampRecords.push(formattedRecord);
484     },
485
486     setSidebarWidth: function(width)
487     {
488         WebInspector.Panel.prototype.setSidebarWidth.call(this, width);
489         this._sidebarBackgroundElement.style.width = width + "px";
490         this._topPaneSidebarElement.style.width = width + "px";
491     },
492
493     updateMainViewWidth: function(width)
494     {
495         this._containerContentElement.style.left = width + "px";
496         this._scheduleRefresh();
497         this._overviewPane.updateMainViewWidth(width);
498     },
499
500     onResize: function()
501     {
502         this._closeRecordDetails();
503         this._scheduleRefresh();
504     },
505
506     _createRootRecord: function()
507     {
508         var rootRecord = {};
509         rootRecord.children = [];
510         rootRecord._visibleRecordsCount = 0;
511         rootRecord._allRecordsCount = 0;
512         rootRecord._aggregatedStats = {};
513         return rootRecord;
514     },
515
516     _clearPanel: function()
517     {
518         this._timeStampRecords = [];
519         this._sendRequestRecords = {};
520         this._scheduledResourceRequests = {};
521         this._timerRecords = {};
522         this._registeredAnimationCallbackRecords = {};
523         this._rootRecord = this._createRootRecord();
524         this._boundariesAreValid = false;
525         this._overviewPane.reset();
526         this._adjustScrollPosition(0);
527         this._refresh();
528         this._closeRecordDetails();
529         this._model._reset();
530     },
531
532     elementsToRestoreScrollPositionsFor: function()
533     {
534         return [this._containerElement];
535     },
536
537     show: function()
538     {
539         WebInspector.Panel.prototype.show.call(this);
540         this._refresh();
541         WebInspector.drawer.currentPanelCounters = this.recordsCounter;
542     },
543
544     hide: function()
545     {
546         WebInspector.Panel.prototype.hide.call(this);
547         this._closeRecordDetails();
548         WebInspector.drawer.currentPanelCounters = null;
549     },
550
551     _onScroll: function(event)
552     {
553         this._closeRecordDetails();
554         var scrollTop = this._containerElement.scrollTop;
555         var dividersTop = Math.max(0, scrollTop);
556         this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
557         this._scheduleRefresh(true);
558     },
559
560     _windowChanged: function()
561     {
562         this._closeRecordDetails();
563         this._scheduleRefresh();
564     },
565
566     _scheduleRefresh: function(preserveBoundaries)
567     {
568         this._closeRecordDetails();
569         this._boundariesAreValid &= preserveBoundaries;
570
571         if (!this.visible)
572             return;
573
574         if (preserveBoundaries)
575             this._refresh();
576         else
577             if (!this._refreshTimeout)
578                 this._refreshTimeout = setTimeout(this._refresh.bind(this), 100);
579     },
580
581     _refresh: function()
582     {
583         if (this._refreshTimeout) {
584             clearTimeout(this._refreshTimeout);
585             delete this._refreshTimeout;
586         }
587
588         this._overviewPane.update(this._rootRecord.children, this._calculator._showShortEvents);
589         this._refreshRecords(!this._boundariesAreValid);
590         this._updateRecordsCounter();
591         if(!this._boundariesAreValid)
592             this._updateEventDividers();
593         this._boundariesAreValid = true;
594     },
595
596     _updateBoundaries: function()
597     {
598         this._calculator.reset();
599         this._calculator.windowLeft = this._overviewPane.windowLeft;
600         this._calculator.windowRight = this._overviewPane.windowRight;
601
602         for (var i = 0; i < this._rootRecord.children.length; ++i)
603             this._calculator.updateBoundaries(this._rootRecord.children[i]);
604
605         this._calculator.calculateWindow();
606     },
607
608     _addToRecordsWindow: function(record, recordsWindow, parentIsCollapsed)
609     {
610         if (!this._calculator._showShortEvents && !record.isLong())
611             return;
612         var percentages = this._calculator.computeBarGraphPercentages(record);
613         if (percentages.start < 100 && percentages.endWithChildren >= 0 && !record.category.hidden) {
614             ++this._rootRecord._visibleRecordsCount;
615             ++record.parent._invisibleChildrenCount;
616             if (!parentIsCollapsed)
617                 recordsWindow.push(record);
618         }
619
620         var index = recordsWindow.length;
621         record._invisibleChildrenCount = 0;
622         for (var i = 0; i < record.children.length; ++i)
623             this._addToRecordsWindow(record.children[i], recordsWindow, parentIsCollapsed || record.collapsed);
624         record._visibleChildrenCount = recordsWindow.length - index;
625     },
626
627     _filterRecords: function()
628     {
629         var recordsInWindow = [];
630         this._rootRecord._visibleRecordsCount = 0;
631         for (var i = 0; i < this._rootRecord.children.length; ++i)
632             this._addToRecordsWindow(this._rootRecord.children[i], recordsInWindow);
633         return recordsInWindow;
634     },
635
636     _refreshRecords: function(updateBoundaries)
637     {
638         if (updateBoundaries)
639             this._updateBoundaries();
640
641         var recordsInWindow = this._filterRecords();
642
643         // Calculate the visible area.
644         this._scrollTop = this._containerElement.scrollTop;
645         var visibleTop = this._scrollTop;
646         var visibleBottom = visibleTop + this._containerElement.clientHeight;
647
648         const rowHeight = WebInspector.TimelinePanel.rowHeight;
649
650         // Convert visible area to visible indexes. Always include top-level record for a visible nested record.
651         var startIndex = Math.max(0, Math.min(Math.floor(visibleTop / rowHeight) - 1, recordsInWindow.length - 1));
652         var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
653
654         // Resize gaps first.
655         const top = (startIndex * rowHeight) + "px";
656         this._topGapElement.style.height = top;
657         this.sidebarElement.style.top = top;
658         this.sidebarResizeElement.style.top = top;
659         this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
660
661         // Update visible rows.
662         var listRowElement = this._sidebarListElement.firstChild;
663         var width = this._graphRowsElement.offsetWidth;
664         this._itemsGraphsElement.removeChild(this._graphRowsElement);
665         var graphRowElement = this._graphRowsElement.firstChild;
666         var scheduleRefreshCallback = this._scheduleRefresh.bind(this, true);
667         this._itemsGraphsElement.removeChild(this._expandElements);
668         this._expandElements.removeChildren();
669
670         for (var i = 0; i < endIndex; ++i) {
671             var record = recordsInWindow[i];
672             var isEven = !(i % 2);
673
674             if (i < startIndex) {
675                 var lastChildIndex = i + record._visibleChildrenCount;
676                 if (lastChildIndex >= startIndex && lastChildIndex < endIndex) {
677                     var expandElement = new WebInspector.TimelineExpandableElement(this._expandElements);
678                     expandElement._update(record, i, this._calculator.computeBarGraphWindowPosition(record, width - this._expandOffset));
679                 }
680             } else {
681                 if (!listRowElement) {
682                     listRowElement = new WebInspector.TimelineRecordListRow().element;
683                     this._sidebarListElement.appendChild(listRowElement);
684                 }
685                 if (!graphRowElement) {
686                     graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, scheduleRefreshCallback, rowHeight).element;
687                     this._graphRowsElement.appendChild(graphRowElement);
688                 }
689
690                 listRowElement.row.update(record, isEven, this._calculator, visibleTop);
691                 graphRowElement.row.update(record, isEven, this._calculator, width, this._expandOffset, i);
692
693                 listRowElement = listRowElement.nextSibling;
694                 graphRowElement = graphRowElement.nextSibling;
695             }
696         }
697
698         // Remove extra rows.
699         while (listRowElement) {
700             var nextElement = listRowElement.nextSibling;
701             listRowElement.row.dispose();
702             listRowElement = nextElement;
703         }
704         while (graphRowElement) {
705             var nextElement = graphRowElement.nextSibling;
706             graphRowElement.row.dispose();
707             graphRowElement = nextElement;
708         }
709
710         this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
711         this._itemsGraphsElement.appendChild(this._expandElements);
712         this.sidebarResizeElement.style.height = this.sidebarElement.clientHeight + "px";
713         // Reserve some room for expand / collapse controls to the left for records that start at 0ms.
714         var timelinePaddingLeft = this._calculator.windowLeft === 0 ? this._expandOffset : 0;
715         if (updateBoundaries)
716             this._timelineGrid.updateDividers(true, this._calculator, timelinePaddingLeft);
717         this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight);
718     },
719
720     _adjustScrollPosition: function(totalHeight)
721     {
722         // Prevent the container from being scrolled off the end.
723         if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1)
724             this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
725     },
726
727     _getPopoverAnchor: function(element)
728     {
729         return element.enclosingNodeOrSelfWithClass("timeline-graph-bar") || element.enclosingNodeOrSelfWithClass("timeline-tree-item");
730     },
731
732     _mouseOut: function(e)
733     {
734         this._hideRectHighlight();
735     },
736
737     _mouseMove: function(e)
738     {
739         var anchor = this._getPopoverAnchor(e.target);
740
741         if (anchor && anchor.row._record.type === "Paint")
742             this._highlightRect(anchor.row._record);
743         else
744             this._hideRectHighlight();
745     },
746
747     _highlightRect: function(record)
748     {
749         if (this._highlightedRect === record.data)
750             return;
751         this._highlightedRect = record.data;
752         DOMAgent.highlightRect(this._highlightedRect.x, this._highlightedRect.y, this._highlightedRect.width, this._highlightedRect.height, WebInspector.Color.PageHighlight.Content.toProtocolRGBA(), WebInspector.Color.PageHighlight.ContentOutline.toProtocolRGBA());
753     },
754
755     _hideRectHighlight: function()
756     {
757         if (this._highlightedRect) {
758             delete this._highlightedRect;
759             DOMAgent.hideHighlight();
760         }
761     },
762
763     _showPopover: function(anchor, popover)
764     {
765         var record = anchor.row._record;
766         popover.show(record._generatePopupContent(this._calculator, this.categories), anchor);
767     },
768
769     _closeRecordDetails: function()
770     {
771         this._popoverHelper.hidePopover();
772     }
773 }
774
775 WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
776
777 WebInspector.TimelineCategory = function(name, title, color)
778 {
779     this.name = name;
780     this.title = title;
781     this.color = color;
782 }
783
784 WebInspector.TimelineCalculator = function()
785 {
786     this.reset();
787     this.windowLeft = 0.0;
788     this.windowRight = 1.0;
789 }
790
791 WebInspector.TimelineCalculator.prototype = {
792     computeBarGraphPercentages: function(record)
793     {
794         var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
795         var end = (record.startTime + record._selfTime - this.minimumBoundary) / this.boundarySpan * 100;
796         var endWithChildren = (record._lastChildEndTime - this.minimumBoundary) / this.boundarySpan * 100;
797         var cpuWidth = record._cpuTime / this.boundarySpan * 100;
798         return {start: start, end: end, endWithChildren: endWithChildren, cpuWidth: cpuWidth};
799     },
800
801     computeBarGraphWindowPosition: function(record, clientWidth)
802     {
803         const minWidth = 5;
804         const borderWidth = 4;
805         var workingArea = clientWidth - minWidth - borderWidth;
806         var percentages = this.computeBarGraphPercentages(record);
807         var left = percentages.start / 100 * workingArea;
808         var width = (percentages.end - percentages.start) / 100 * workingArea + minWidth;
809         var widthWithChildren =  (percentages.endWithChildren - percentages.start) / 100 * workingArea;
810         var cpuWidth = percentages.cpuWidth / 100 * workingArea + minWidth;
811         if (percentages.endWithChildren > percentages.end)
812             widthWithChildren += borderWidth + minWidth;
813         return {left: left, width: width, widthWithChildren: widthWithChildren, cpuWidth: cpuWidth};
814     },
815
816     calculateWindow: function()
817     {
818         this.minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
819         this.maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
820         this.boundarySpan = this.maximumBoundary - this.minimumBoundary;
821     },
822
823     reset: function()
824     {
825         this._absoluteMinimumBoundary = -1;
826         this._absoluteMaximumBoundary = -1;
827     },
828
829     updateBoundaries: function(record)
830     {
831         var lowerBound = record.startTime;
832         if (this._absoluteMinimumBoundary === -1 || lowerBound < this._absoluteMinimumBoundary)
833             this._absoluteMinimumBoundary = lowerBound;
834
835         const minimumTimeFrame = 0.1;
836         const minimumDeltaForZeroSizeEvents = 0.01;
837         var upperBound = Math.max(record._lastChildEndTime + minimumDeltaForZeroSizeEvents, lowerBound + minimumTimeFrame);
838         if (this._absoluteMaximumBoundary === -1 || upperBound > this._absoluteMaximumBoundary)
839             this._absoluteMaximumBoundary = upperBound;
840     },
841
842     formatValue: function(value)
843     {
844         return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary);
845     }
846 }
847
848
849 WebInspector.TimelineRecordListRow = function()
850 {
851     this.element = document.createElement("div");
852     this.element.row = this;
853     this.element.style.cursor = "pointer";
854     var iconElement = document.createElement("span");
855     iconElement.className = "timeline-tree-icon";
856     this.element.appendChild(iconElement);
857
858     this._typeElement = document.createElement("span");
859     this._typeElement.className = "type";
860     this.element.appendChild(this._typeElement);
861
862     var separatorElement = document.createElement("span");
863     separatorElement.className = "separator";
864     separatorElement.textContent = " ";
865
866     this._dataElement = document.createElement("span");
867     this._dataElement.className = "data dimmed";
868
869     this.element.appendChild(separatorElement);
870     this.element.appendChild(this._dataElement);
871 }
872
873 WebInspector.TimelineRecordListRow.prototype = {
874     update: function(record, isEven, calculator, offset)
875     {
876         this._record = record;
877         this._calculator = calculator;
878         this._offset = offset;
879
880         this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : "");
881         this._typeElement.textContent = record.title;
882
883         if (this._dataElement.firstChild)
884             this._dataElement.removeChildren();
885         if (record.details) {
886             var detailsContainer = document.createElement("span");
887             if (typeof record.details === "object") {
888                 detailsContainer.appendChild(document.createTextNode("("));
889                 detailsContainer.appendChild(record.details);
890                 detailsContainer.appendChild(document.createTextNode(")"));
891             } else
892                 detailsContainer.textContent = "(" + record.details + ")";
893             this._dataElement.appendChild(detailsContainer);
894         }
895     },
896
897     dispose: function()
898     {
899         this.element.parentElement.removeChild(this.element);
900     }
901 }
902
903 WebInspector.TimelineRecordGraphRow = function(graphContainer, scheduleRefresh)
904 {
905     this.element = document.createElement("div");
906     this.element.row = this;
907
908     this._barAreaElement = document.createElement("div");
909     this._barAreaElement.className = "timeline-graph-bar-area";
910     this.element.appendChild(this._barAreaElement);
911
912     this._barWithChildrenElement = document.createElement("div");
913     this._barWithChildrenElement.className = "timeline-graph-bar with-children";
914     this._barWithChildrenElement.row = this;
915     this._barAreaElement.appendChild(this._barWithChildrenElement);
916
917     this._barCpuElement = document.createElement("div");
918     this._barCpuElement.className = "timeline-graph-bar cpu"
919     this._barCpuElement.row = this;
920     this._barAreaElement.appendChild(this._barCpuElement);
921
922     this._barElement = document.createElement("div");
923     this._barElement.className = "timeline-graph-bar";
924     this._barElement.row = this;
925     this._barAreaElement.appendChild(this._barElement);
926
927     this._expandElement = new WebInspector.TimelineExpandableElement(graphContainer);
928     this._expandElement._element.addEventListener("click", this._onClick.bind(this));
929
930     this._scheduleRefresh = scheduleRefresh;
931 }
932
933 WebInspector.TimelineRecordGraphRow.prototype = {
934     update: function(record, isEven, calculator, clientWidth, expandOffset, index)
935     {
936         this._record = record;
937         this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : "");
938         var barPosition = calculator.computeBarGraphWindowPosition(record, clientWidth - expandOffset);
939         this._barWithChildrenElement.style.left = barPosition.left + expandOffset + "px";
940         this._barWithChildrenElement.style.width = barPosition.widthWithChildren + "px";
941         this._barElement.style.left = barPosition.left + expandOffset + "px";
942         this._barElement.style.width =  barPosition.width + "px";
943         this._barCpuElement.style.left = barPosition.left + expandOffset + "px";
944         this._barCpuElement.style.width = barPosition.cpuWidth + "px";
945         this._expandElement._update(record, index, barPosition);
946     },
947
948     _onClick: function(event)
949     {
950         this._record.collapsed = !this._record.collapsed;
951         this._scheduleRefresh();
952     },
953
954     dispose: function()
955     {
956         this.element.parentElement.removeChild(this.element);
957         this._expandElement._dispose();
958     }
959 }
960
961 WebInspector.TimelinePanel.FormattedRecord = function(record, parentRecord, panel, scriptDetails)
962 {
963     var recordTypes = WebInspector.TimelineAgent.RecordType;
964     var style = panel._recordStyles[record.type];
965     this.parent = parentRecord;
966     if (parentRecord)
967         parentRecord.children.push(this);
968     this.category = style.category;
969     this.title = style.title;
970     this.startTime = record.startTime / 1000;
971     this.data = record.data;
972     this.type = record.type;
973     this.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : this.startTime;
974     this._selfTime = this.endTime - this.startTime;
975     this._lastChildEndTime = this.endTime;
976     if (record.stackTrace && record.stackTrace.length)
977         this.stackTrace = record.stackTrace;
978     this.totalHeapSize = record.totalHeapSize;
979     this.usedHeapSize = record.usedHeapSize;
980     if (record.data && record.data.url)
981         this.url = record.data.url;
982     if (scriptDetails) {
983         this.scriptName = scriptDetails.scriptName;
984         this.scriptLine = scriptDetails.scriptLine;
985     }
986     // Make resource receive record last since request was sent; make finish record last since response received.
987     if (record.type === recordTypes.ResourceSendRequest) {
988         panel._sendRequestRecords[record.data.requestId] = this;
989     } else if (record.type === recordTypes.ScheduleResourceRequest) {
990         panel._scheduledResourceRequests[record.data.url] = this;
991     } else if (record.type === recordTypes.ResourceReceiveResponse) {
992         var sendRequestRecord = panel._sendRequestRecords[record.data.requestId];
993         if (sendRequestRecord) { // False if we started instrumentation in the middle of request.
994             this.url = sendRequestRecord.url;
995             // Now that we have resource in the collection, recalculate details in order to display short url.
996             sendRequestRecord._refreshDetails();
997             if (sendRequestRecord.parent !== panel._rootRecord && sendRequestRecord.parent.type === recordTypes.ScheduleResourceRequest)
998                 sendRequestRecord.parent._refreshDetails();
999         }
1000     } else if (record.type === recordTypes.ResourceReceivedData || record.type === recordTypes.ResourceFinish) {
1001         var sendRequestRecord = panel._sendRequestRecords[record.data.requestId];
1002         if (sendRequestRecord) // False for main resource.
1003             this.url = sendRequestRecord.url;
1004     } else if (record.type === recordTypes.TimerInstall) {
1005         this.timeout = record.data.timeout;
1006         this.singleShot = record.data.singleShot;
1007         panel._timerRecords[record.data.timerId] = this;
1008     } else if (record.type === recordTypes.TimerFire) {
1009         var timerInstalledRecord = panel._timerRecords[record.data.timerId];
1010         if (timerInstalledRecord) {
1011             this.callSiteStackTrace = timerInstalledRecord.stackTrace;
1012             this.timeout = timerInstalledRecord.timeout;
1013             this.singleShot = timerInstalledRecord.singleShot;
1014         }
1015     } else if (record.type === recordTypes.FireAnimationFrameEvent) {
1016         var registerCallbackRecord = panel._registeredAnimationCallbackRecords[record.data.id];
1017         if (registerCallbackRecord)
1018             this.callSiteStackTrace = registerCallbackRecord.stackTrace;
1019     }
1020     this._refreshDetails();
1021 }
1022
1023 WebInspector.TimelinePanel.FormattedRecord.prototype = {
1024     isLong: function()
1025     {
1026         return (this._lastChildEndTime - this.startTime) > WebInspector.TimelinePanel.shortRecordThreshold;
1027     },
1028
1029     get children()
1030     {
1031         if (!this._children)
1032             this._children = [];
1033         return this._children;
1034     },
1035
1036     _generateAggregatedInfo: function()
1037     {
1038         var cell = document.createElement("span");
1039         cell.className = "timeline-aggregated-info";
1040         for (var index in this._aggregatedStats) {
1041             var label = document.createElement("div");
1042             label.className = "timeline-aggregated-category timeline-" + index;
1043             cell.appendChild(label);
1044             var text = document.createElement("span");
1045             text.textContent = Number.secondsToString(this._aggregatedStats[index] + 0.0001);
1046             cell.appendChild(text);
1047         }
1048         return cell;
1049     },
1050
1051     _generatePopupContent: function(calculator, categories)
1052     {
1053         var contentHelper = new WebInspector.TimelinePanel.PopupContentHelper(this.title);
1054
1055         if (this._children && this._children.length) {
1056             contentHelper._appendTextRow(WebInspector.UIString("Self Time"), Number.secondsToString(this._selfTime + 0.0001));
1057             contentHelper._appendElementRow(WebInspector.UIString("Aggregated Time"), this._generateAggregatedInfo());
1058         }
1059         var text = WebInspector.UIString("%s (at %s)", Number.secondsToString(this._lastChildEndTime - this.startTime),
1060             calculator.formatValue(this.startTime - calculator.minimumBoundary));
1061         contentHelper._appendTextRow(WebInspector.UIString("Duration"), text);
1062
1063         const recordTypes = WebInspector.TimelineAgent.RecordType;
1064
1065         switch (this.type) {
1066             case recordTypes.GCEvent:
1067                 contentHelper._appendTextRow(WebInspector.UIString("Collected"), Number.bytesToString(this.data.usedHeapSizeDelta));
1068                 break;
1069             case recordTypes.TimerInstall:
1070             case recordTypes.TimerFire:
1071             case recordTypes.TimerRemove:
1072                 contentHelper._appendTextRow(WebInspector.UIString("Timer ID"), this.data.timerId);
1073                 if (typeof this.timeout === "number") {
1074                     contentHelper._appendTextRow(WebInspector.UIString("Timeout"), Number.secondsToString(this.timeout / 1000));
1075                     contentHelper._appendTextRow(WebInspector.UIString("Repeats"), !this.singleShot);
1076                 }
1077                 break;
1078             case recordTypes.FireAnimationFrameEvent:
1079                 contentHelper._appendTextRow(WebInspector.UIString("Callback ID"), this.data.id);
1080                 break;
1081             case recordTypes.FunctionCall:
1082                 contentHelper._appendLinkRow(WebInspector.UIString("Location"), this.scriptName, this.scriptLine);
1083                 break;
1084             case recordTypes.ScheduleResourceRequest:
1085             case recordTypes.ResourceSendRequest:
1086             case recordTypes.ResourceReceiveResponse:
1087             case recordTypes.ResourceReceivedData:
1088             case recordTypes.ResourceFinish:
1089                 contentHelper._appendLinkRow(WebInspector.UIString("Resource"), this.url);
1090                 if (this.data.requestMethod)
1091                     contentHelper._appendTextRow(WebInspector.UIString("Request Method"), this.data.requestMethod);
1092                 if (typeof this.data.statusCode === "number")
1093                     contentHelper._appendTextRow(WebInspector.UIString("Status Code"), this.data.statusCode);
1094                 if (this.data.mimeType)
1095                     contentHelper._appendTextRow(WebInspector.UIString("MIME Type"), this.data.mimeType);
1096                 break;
1097             case recordTypes.EvaluateScript:
1098                 if (this.data && this.url)
1099                     contentHelper._appendLinkRow(WebInspector.UIString("Script"), this.url, this.data.lineNumber);
1100                 break;
1101             case recordTypes.Paint:
1102                 contentHelper._appendTextRow(WebInspector.UIString("Location"), WebInspector.UIString("(%d, %d)", this.data.x, this.data.y));
1103                 contentHelper._appendTextRow(WebInspector.UIString("Dimensions"), WebInspector.UIString("%d × %d", this.data.width, this.data.height));
1104             case recordTypes.RecalculateStyles: // We don't want to see default details.
1105                 break;
1106             default:
1107                 if (this.details)
1108                     contentHelper._appendTextRow(WebInspector.UIString("Details"), this.details);
1109                 break;
1110         }
1111
1112         if (this.scriptName && this.type !== recordTypes.FunctionCall)
1113             contentHelper._appendLinkRow(WebInspector.UIString("Function Call"), this.scriptName, this.scriptLine);
1114
1115         if (this.usedHeapSize)
1116             contentHelper._appendTextRow(WebInspector.UIString("Used Heap Size"), WebInspector.UIString("%s of %s", Number.bytesToString(this.usedHeapSize), Number.bytesToString(this.totalHeapSize)));
1117
1118         if (this.callSiteStackTrace && this.callSiteStackTrace.length)
1119             contentHelper._appendStackTrace(WebInspector.UIString("Call Site stack"), this.callSiteStackTrace);
1120
1121         if (this.stackTrace)
1122             contentHelper._appendStackTrace(WebInspector.UIString("Call Stack"), this.stackTrace);
1123
1124         return contentHelper._contentTable;
1125     },
1126
1127     _refreshDetails: function()
1128     {
1129         this.details = this._getRecordDetails();
1130     },
1131
1132     _getRecordDetails: function()
1133     {
1134         switch (this.type) {
1135             case WebInspector.TimelineAgent.RecordType.GCEvent:
1136                 return WebInspector.UIString("%s collected", Number.bytesToString(this.data.usedHeapSizeDelta));
1137             case WebInspector.TimelineAgent.RecordType.TimerFire:
1138                 return this.scriptName ? this._linkifyLocation(this.scriptName, this.scriptLine, 0) : this.data.timerId;
1139             case WebInspector.TimelineAgent.RecordType.FunctionCall:
1140                 return this.scriptName ? this._linkifyLocation(this.scriptName, this.scriptLine, 0) : null;
1141             case WebInspector.TimelineAgent.RecordType.FireAnimationFrameEvent:
1142                 return this.scriptName ? this._linkifyLocation(this.scriptName, this.scriptLine, 0) : this.data.id;
1143             case WebInspector.TimelineAgent.RecordType.EventDispatch:
1144                 return this.data ? this.data.type : null;
1145             case WebInspector.TimelineAgent.RecordType.Paint:
1146                 return this.data.width + "\u2009\u00d7\u2009" + this.data.height;
1147             case WebInspector.TimelineAgent.RecordType.TimerInstall:
1148             case WebInspector.TimelineAgent.RecordType.TimerRemove:
1149                 return this.stackTrace ? this._linkifyCallFrame(this.stackTrace[0]) : this.data.timerId;
1150             case WebInspector.TimelineAgent.RecordType.RegisterAnimationFrameCallback:
1151             case WebInspector.TimelineAgent.RecordType.CancelAnimationFrameCallback:
1152                 return this.stackTrace ? this._linkifyCallFrame(this.stackTrace[0]) : this.data.id;
1153             case WebInspector.TimelineAgent.RecordType.ParseHTML:
1154             case WebInspector.TimelineAgent.RecordType.RecalculateStyles:
1155                 return this.stackTrace ? this._linkifyCallFrame(this.stackTrace[0]) : null;
1156             case WebInspector.TimelineAgent.RecordType.EvaluateScript:
1157                 return this.url ? this._linkifyLocation(this.url, this.data.lineNumber, 0) : null;
1158             case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange:
1159             case WebInspector.TimelineAgent.RecordType.XHRLoad:
1160             case WebInspector.TimelineAgent.RecordType.ScheduleResourceRequest:
1161             case WebInspector.TimelineAgent.RecordType.ResourceSendRequest:
1162             case WebInspector.TimelineAgent.RecordType.ResourceReceivedData:
1163             case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse:
1164             case WebInspector.TimelineAgent.RecordType.ResourceFinish:
1165                 return WebInspector.displayNameForURL(this.url);
1166             case WebInspector.TimelineAgent.RecordType.TimeStamp:
1167                 return this.data.message;
1168             default:
1169                 return null;
1170         }
1171     },
1172
1173     _linkifyLocation: function(url, lineNumber, columnNumber)
1174     {
1175         // FIXME(62725): stack trace line/column numbers are one-based.
1176         lineNumber = lineNumber ? lineNumber - 1 : lineNumber;
1177         columnNumber = columnNumber ? columnNumber - 1 : 0;
1178         return WebInspector.debuggerPresentationModel.linkifyLocation(url, lineNumber, columnNumber, "timeline-details");
1179     },
1180
1181     _linkifyCallFrame: function(callFrame)
1182     {
1183         return this._linkifyLocation(callFrame.url, callFrame.lineNumber, callFrame.columnNumber);
1184     },
1185
1186     _calculateAggregatedStats: function(categories)
1187     {
1188         this._aggregatedStats = {};
1189         for (var category in categories)
1190             this._aggregatedStats[category] = 0;
1191         this._cpuTime = this._selfTime;
1192
1193         if (this._children) {
1194             for (var index = this._children.length; index; --index) {
1195                 var child = this._children[index - 1];
1196                 this._aggregatedStats[child.category.name] += child._selfTime;
1197                 for (var category in categories)
1198                     this._aggregatedStats[category] += child._aggregatedStats[category];
1199             }
1200             for (var category in this._aggregatedStats)
1201                 this._cpuTime += this._aggregatedStats[category];
1202         }
1203     }
1204 }
1205
1206 WebInspector.TimelinePanel.PopupContentHelper = function(title)
1207 {
1208     this._contentTable = document.createElement("table");;
1209     var titleCell = this._createCell(WebInspector.UIString("%s - Details", title), "timeline-details-title");
1210     titleCell.colSpan = 2;
1211     var titleRow = document.createElement("tr");
1212     titleRow.appendChild(titleCell);
1213     this._contentTable.appendChild(titleRow);
1214 }
1215
1216 WebInspector.TimelinePanel.PopupContentHelper.prototype = {
1217     _createCell: function(content, styleName)
1218     {
1219         var text = document.createElement("label");
1220         text.appendChild(document.createTextNode(content));
1221         var cell = document.createElement("td");
1222         cell.className = "timeline-details";
1223         if (styleName)
1224             cell.className += " " + styleName;
1225         cell.textContent = content;
1226         return cell;
1227     },
1228
1229     _appendTextRow: function(title, content)
1230     {
1231         var row = document.createElement("tr");
1232         row.appendChild(this._createCell(title, "timeline-details-row-title"));
1233         row.appendChild(this._createCell(content, "timeline-details-row-data"));
1234         this._contentTable.appendChild(row);
1235     },
1236
1237     _appendElementRow: function(title, content, titleStyle)
1238     {
1239         var row = document.createElement("tr");
1240         var titleCell = this._createCell(title, "timeline-details-row-title");
1241         if (titleStyle)
1242             titleCell.addStyleClass(titleStyle);
1243         row.appendChild(titleCell);
1244         var cell = document.createElement("td");
1245         cell.className = "timeline-details";
1246         cell.appendChild(content);
1247         row.appendChild(cell);
1248         this._contentTable.appendChild(row);
1249     },
1250
1251     _appendLinkRow: function(title, scriptName, scriptLine)
1252     {
1253         var link = WebInspector.TimelinePanel.FormattedRecord.prototype._linkifyLocation(scriptName, scriptLine, 0, "timeline-details");
1254         this._appendElementRow(title, link);
1255     },
1256
1257     _appendStackTrace: function(title, stackTrace)
1258     {
1259         this._appendTextRow("", "");
1260         var framesTable = document.createElement("table");
1261         for (var i = 0; i < stackTrace.length; ++i) {
1262             var stackFrame = stackTrace[i];
1263             var row = document.createElement("tr");
1264             row.className = "timeline-details";
1265             row.appendChild(this._createCell(stackFrame.functionName ? stackFrame.functionName : WebInspector.UIString("(anonymous function)"), "timeline-function-name"));
1266             row.appendChild(this._createCell(" @ "));
1267             var linkCell = document.createElement("td");
1268             var urlElement = WebInspector.TimelinePanel.FormattedRecord.prototype._linkifyCallFrame(stackFrame);
1269             linkCell.appendChild(urlElement);
1270             row.appendChild(linkCell);
1271             framesTable.appendChild(row);
1272         }
1273         this._appendElementRow(title, framesTable, "timeline-stacktrace-title");
1274     }
1275 }
1276
1277 WebInspector.TimelineExpandableElement = function(container)
1278 {
1279     this._element = document.createElement("div");
1280     this._element.className = "timeline-expandable";
1281
1282     var leftBorder = document.createElement("div");
1283     leftBorder.className = "timeline-expandable-left";
1284     this._element.appendChild(leftBorder);
1285
1286     container.appendChild(this._element);
1287 }
1288
1289 WebInspector.TimelineExpandableElement.prototype = {
1290     _update: function(record, index, barPosition)
1291     {
1292         const rowHeight = WebInspector.TimelinePanel.rowHeight;
1293         if (record._visibleChildrenCount || record._invisibleChildrenCount) {
1294             this._element.style.top = index * rowHeight + "px";
1295             this._element.style.left = barPosition.left + "px";
1296             this._element.style.width = Math.max(12, barPosition.width + 25) + "px";
1297             if (!record.collapsed) {
1298                 this._element.style.height = (record._visibleChildrenCount + 1) * rowHeight + "px";
1299                 this._element.addStyleClass("timeline-expandable-expanded");
1300                 this._element.removeStyleClass("timeline-expandable-collapsed");
1301             } else {
1302                 this._element.style.height = rowHeight + "px";
1303                 this._element.addStyleClass("timeline-expandable-collapsed");
1304                 this._element.removeStyleClass("timeline-expandable-expanded");
1305             }
1306             this._element.removeStyleClass("hidden");
1307         } else
1308             this._element.addStyleClass("hidden");
1309     },
1310
1311     _dispose: function()
1312     {
1313         this._element.parentElement.removeChild(this._element);
1314     }
1315 }
1316
1317 WebInspector.TimelineModel = function(timelinePanel)
1318 {
1319     this._panel = timelinePanel;
1320     this._records = [];
1321 }
1322
1323 WebInspector.TimelineModel.prototype = {
1324     _addRecord: function(record)
1325     {
1326         this._records.push(record);
1327     },
1328
1329     _loadNextChunk: function(data, index)
1330     {
1331         for (var i = 0; i < 20 && index < data.length; ++i, ++index)
1332             this._panel._addRecordToTimeline(data[index]);
1333
1334         if (index !== data.length)
1335             setTimeout(this._loadNextChunk.bind(this, data, index), 0);
1336     },
1337
1338     _loadFromFile: function(file)
1339     {
1340         function onLoad(e)
1341         {
1342             var data = JSON.parse(e.target.result);
1343             var version = data[0];
1344             this._loadNextChunk(data, 1);
1345         }
1346
1347         function onError(e)
1348         {
1349             switch(e.target.error.code) {
1350             case e.target.error.NOT_FOUND_ERR:
1351                 WebInspector.log(WebInspector.UIString('Timeline.loadFromFile: File "%s" not found.', file.name));
1352             break;
1353             case e.target.error.NOT_READABLE_ERR:
1354                 WebInspector.log(WebInspector.UIString('Timeline.loadFromFile: File "%s" is not readable', file.name));
1355             break;
1356             case e.target.error.ABORT_ERR:
1357                 break;
1358             default:
1359                 WebInspector.log(WebInspector.UIString('Timeline.loadFromFile: An error occurred while reading the file "%s"', file.name));
1360             }
1361         }
1362
1363         var reader = new FileReader();
1364         reader.onload = onLoad.bind(this);
1365         reader.onerror = onError;
1366         reader.readAsText(file);
1367     },
1368
1369     _saveToFile: function()
1370     {
1371         var records = ['[' + JSON.stringify(window.navigator.appVersion)];
1372         for (var i = 0; i < this._records.length; ++i)
1373             records.push(JSON.stringify(this._records[i]));
1374             records[records.length - 1] = records[records.length - 1] + "]";
1375
1376         var now= new Date();
1377         var suggestedFileName = "TimelineRawData-" + now.toISO8601Compact() + ".json";
1378         InspectorFrontendHost.saveAs(suggestedFileName, records.join(",\n"));
1379     },
1380
1381     _reset: function()
1382     {
1383         this._records = [];
1384     }
1385 }