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