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