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