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