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