Home | History | Annotate | Download | only in front-end
      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);
     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.
    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.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 
    652 WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
    653 
    654 WebInspector.TimelineDispatcher = function(timelinePanel)
    655 {
    656     this._timelinePanel = timelinePanel;
    657 }
    658 
    659 WebInspector.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 
    676 WebInspector.TimelineCategory = function(name, title, color)
    677 {
    678     this.name = name;
    679     this.title = title;
    680     this.color = color;
    681 }
    682 
    683 WebInspector.TimelineCalculator = function()
    684 {
    685     this.reset();
    686     this.windowLeft = 0.0;
    687     this.windowRight = 1.0;
    688 }
    689 
    690 WebInspector.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 
    748 WebInspector.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 
    772 WebInspector.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 
    802 WebInspector.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 
    832 WebInspector.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 
    860 WebInspector.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 
    917 WebInspector.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 
   1074 WebInspector.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 
   1084 WebInspector.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 
   1144 WebInspector.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 
   1156 WebInspector.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