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