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