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);
     34     this.element.addStyleClass("timeline");
     35 
     36     this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories);
     37     this._overviewPane.addEventListener("window changed", this._windowChanged, this);
     38     this._overviewPane.addEventListener("filter changed", this._refresh, this);
     39     this.element.appendChild(this._overviewPane.element);
     40 
     41     this._sidebarBackgroundElement = document.createElement("div");
     42     this._sidebarBackgroundElement.className = "sidebar timeline-sidebar-background";
     43     this.element.appendChild(this._sidebarBackgroundElement);
     44 
     45     this._containerElement = document.createElement("div");
     46     this._containerElement.id = "timeline-container";
     47     this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
     48     this.element.appendChild(this._containerElement);
     49 
     50     this.createSidebar(this._containerElement, this._containerElement);
     51     var itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
     52     itemsTreeElement.expanded = true;
     53     this.sidebarTree.appendChild(itemsTreeElement);
     54 
     55     this._sidebarListElement = document.createElement("div");
     56     this.sidebarElement.appendChild(this._sidebarListElement);
     57 
     58     this._containerContentElement = document.createElement("div");
     59     this._containerContentElement.id = "resources-container-content";
     60     this._containerElement.appendChild(this._containerContentElement);
     61 
     62     this._timelineGrid = new WebInspector.TimelineGrid();
     63     this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
     64     this._itemsGraphsElement.id = "timeline-graphs";
     65     this._containerContentElement.appendChild(this._timelineGrid.element);
     66 
     67     this._topGapElement = document.createElement("div");
     68     this._topGapElement.className = "timeline-gap";
     69     this._itemsGraphsElement.appendChild(this._topGapElement);
     70 
     71     this._graphRowsElement = document.createElement("div");
     72     this._itemsGraphsElement.appendChild(this._graphRowsElement);
     73 
     74     this._bottomGapElement = document.createElement("div");
     75     this._bottomGapElement.className = "timeline-gap";
     76     this._itemsGraphsElement.appendChild(this._bottomGapElement);
     77 
     78     this._createStatusbarButtons();
     79 
     80     this._records = [];
     81     this._sendRequestRecords = {};
     82     this._calculator = new WebInspector.TimelineCalculator();
     83     this._boundariesAreValid = true;
     84 }
     85 
     86 WebInspector.TimelinePanel.prototype = {
     87     toolbarItemClass: "timeline",
     88 
     89     get toolbarItemLabel()
     90     {
     91         return WebInspector.UIString("Timeline");
     92     },
     93 
     94     get statusBarItems()
     95     {
     96         return [this.toggleTimelineButton.element, this.clearButton.element];
     97     },
     98 
     99     get categories()
    100     {
    101         if (!this._categories) {
    102             this._categories = {
    103                 loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"),
    104                 scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"),
    105                 rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)")
    106             };
    107         }
    108         return this._categories;
    109     },
    110 
    111     _createStatusbarButtons: function()
    112     {
    113         this.toggleTimelineButton = new WebInspector.StatusBarButton("", "record-profile-status-bar-item");
    114         this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false);
    115 
    116         this.clearButton = new WebInspector.StatusBarButton("", "timeline-clear-status-bar-item");
    117         this.clearButton.addEventListener("click", this.reset.bind(this), false);
    118     },
    119 
    120     _toggleTimelineButtonClicked: function()
    121     {
    122         if (this.toggleTimelineButton.toggled)
    123             InspectorBackend.stopTimelineProfiler();
    124         else
    125             InspectorBackend.startTimelineProfiler();
    126     },
    127 
    128     timelineWasStarted: function()
    129     {
    130         this.toggleTimelineButton.toggled = true;
    131     },
    132 
    133     timelineWasStopped: function()
    134     {
    135         this.toggleTimelineButton.toggled = false;
    136     },
    137 
    138     addRecordToTimeline: function(record)
    139     {
    140         this._innerAddRecordToTimeline(record, this._records);
    141         this._scheduleRefresh();
    142     },
    143 
    144     _innerAddRecordToTimeline: function(record, collection)
    145     {
    146         var formattedRecord = this._formatRecord(record);
    147 
    148         // Glue subsequent records with same category and title together if they are closer than 100ms to each other.
    149         if (this._lastRecord && (!record.children || !record.children.length) &&
    150                 this._lastRecord.category == formattedRecord.category &&
    151                 this._lastRecord.title == formattedRecord.title &&
    152                 this._lastRecord.details == formattedRecord.details &&
    153                 formattedRecord.startTime - this._lastRecord.endTime < 0.1) {
    154             this._lastRecord.endTime = formattedRecord.endTime;
    155             this._lastRecord.count++;
    156         } else {
    157             collection.push(formattedRecord);
    158             for (var i = 0; record.children && i < record.children.length; ++i) {
    159                 if (!formattedRecord.children)
    160                     formattedRecord.children = [];
    161                 var formattedChild = this._innerAddRecordToTimeline(record.children[i], formattedRecord.children);
    162                 formattedChild.parent = formattedRecord;
    163             }
    164             this._lastRecord = record.children && record.children.length ? null : formattedRecord;
    165         }
    166         return formattedRecord;
    167     },
    168 
    169     _formatRecord: function(record)
    170     {
    171         var recordTypes = WebInspector.TimelineAgent.RecordType;
    172         if (!this._recordStyles) {
    173             this._recordStyles = {};
    174             this._recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting };
    175             this._recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering };
    176             this._recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering };
    177             this._recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering };
    178             this._recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading };
    179             this._recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting };
    180             this._recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting };
    181             this._recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting };
    182             this._recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting };
    183             this._recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting };
    184             this._recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting };
    185             this._recordStyles[recordTypes.MarkTimeline] = { title: WebInspector.UIString("Mark"), category: this.categories.scripting };
    186             this._recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading };
    187             this._recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading };
    188             this._recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading };
    189         }
    190 
    191         var style = this._recordStyles[record.type];
    192         if (!style)
    193             style = this._recordStyles[recordTypes.EventDispatch];
    194 
    195         var formattedRecord = {};
    196         formattedRecord.category = style.category;
    197         formattedRecord.title = style.title;
    198         formattedRecord.startTime = record.startTime / 1000;
    199         formattedRecord.data = record.data;
    200         formattedRecord.count = 1;
    201         formattedRecord.type = record.type;
    202         formattedRecord.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : formattedRecord.startTime;
    203         formattedRecord.record = record;
    204 
    205         // Make resource receive record last since request was sent; make finish record last since response received.
    206         if (record.type === WebInspector.TimelineAgent.RecordType.ResourceSendRequest) {
    207             this._sendRequestRecords[record.data.identifier] = formattedRecord;
    208         } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse) {
    209             var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
    210             if (sendRequestRecord) { // False if we started instrumentation in the middle of request.
    211                 sendRequestRecord._responseReceivedFormattedTime = formattedRecord.startTime;
    212                 formattedRecord.startTime = sendRequestRecord.startTime;
    213                 sendRequestRecord.details = this._getRecordDetails(record);
    214             }
    215         } else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceFinish) {
    216             var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
    217             if (sendRequestRecord) // False for main resource.
    218                 formattedRecord.startTime = sendRequestRecord._responseReceivedFormattedTime;
    219         }
    220         formattedRecord.details = this._getRecordDetails(record);
    221 
    222         return formattedRecord;
    223     },
    224 
    225     _getRecordDetails: function(record)
    226     {
    227         switch (record.type) {
    228         case WebInspector.TimelineAgent.RecordType.EventDispatch:
    229             return record.data ? record.data.type : "";
    230         case WebInspector.TimelineAgent.RecordType.Paint:
    231             return record.data.width + "\u2009\u00d7\u2009" + record.data.height;
    232         case WebInspector.TimelineAgent.RecordType.TimerInstall:
    233         case WebInspector.TimelineAgent.RecordType.TimerRemove:
    234         case WebInspector.TimelineAgent.RecordType.TimerFire:
    235             return record.data.timerId;
    236         case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange:
    237         case WebInspector.TimelineAgent.RecordType.XHRLoad:
    238         case WebInspector.TimelineAgent.RecordType.EvaluateScript:
    239         case WebInspector.TimelineAgent.RecordType.ResourceSendRequest:
    240             return WebInspector.displayNameForURL(record.data.url);
    241         case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse:
    242         case WebInspector.TimelineAgent.RecordType.ResourceFinish:
    243             var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
    244             return sendRequestRecord ? WebInspector.displayNameForURL(sendRequestRecord.data.url) : "";
    245         case WebInspector.TimelineAgent.RecordType.MarkTimeline:
    246             return record.data.message;
    247         default:
    248             return "";
    249         }
    250     },
    251 
    252     setSidebarWidth: function(width)
    253     {
    254         WebInspector.Panel.prototype.setSidebarWidth.call(this, width);
    255         this._sidebarBackgroundElement.style.width = width + "px";
    256         this._overviewPane.setSidebarWidth(width);
    257     },
    258 
    259     updateMainViewWidth: function(width)
    260     {
    261         this._containerContentElement.style.left = width + "px";
    262         this._scheduleRefresh();
    263         this._overviewPane.updateMainViewWidth(width);
    264     },
    265 
    266     resize: function() {
    267         this._scheduleRefresh();
    268     },
    269 
    270     reset: function()
    271     {
    272         this._lastRecord = null;
    273         this._sendRequestRecords = {};
    274         this._records = [];
    275         this._boundariesAreValid = false;
    276         this._overviewPane.reset();
    277         this._adjustScrollPosition(0);
    278         this._refresh();
    279     },
    280 
    281     show: function()
    282     {
    283         WebInspector.Panel.prototype.show.call(this);
    284 
    285         if (this._needsRefresh)
    286             this._refresh();
    287     },
    288 
    289     _onScroll: function(event)
    290     {
    291         var scrollTop = this._containerElement.scrollTop;
    292         var dividersTop = Math.max(0, scrollTop);
    293         this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
    294         this._scheduleRefresh(true);
    295     },
    296 
    297     _windowChanged: function()
    298     {
    299         this._scheduleRefresh();
    300     },
    301 
    302     _scheduleRefresh: function(preserveBoundaries)
    303     {
    304         this._boundariesAreValid &= preserveBoundaries;
    305         if (this._needsRefresh)
    306             return;
    307         this._needsRefresh = true;
    308 
    309         if (this.visible && !("_refreshTimeout" in this)) {
    310             if (preserveBoundaries)
    311                 this._refresh();
    312             else
    313                 this._refreshTimeout = setTimeout(this._refresh.bind(this), 100);
    314         }
    315     },
    316 
    317     _refresh: function()
    318     {
    319         this._needsRefresh = false;
    320         if ("_refreshTimeout" in this) {
    321             clearTimeout(this._refreshTimeout);
    322             delete this._refreshTimeout;
    323         }
    324 
    325         if (!this._boundariesAreValid)
    326             this._overviewPane.update(this._records);
    327         this._refreshRecords(!this._boundariesAreValid);
    328         this._boundariesAreValid = true;
    329     },
    330 
    331     _refreshRecords: function(updateBoundaries)
    332     {
    333         if (updateBoundaries) {
    334             this._calculator.reset();
    335             this._calculator.windowLeft = this._overviewPane.windowLeft;
    336             this._calculator.windowRight = this._overviewPane.windowRight;
    337 
    338             for (var i = 0; i < this._records.length; ++i)
    339                 this._calculator.updateBoundaries(this._records[i]);
    340 
    341             this._calculator.calculateWindow();
    342         }
    343 
    344         var recordsInWindow = [];
    345         for (var i = 0; i < this._records.length; ++i) {
    346             var record = this._records[i];
    347             var percentages = this._calculator.computeBarGraphPercentages(record);
    348             if (percentages.start < 100 && percentages.end >= 0 && !record.category.hidden)
    349                 this._addToRecordsWindow(record, recordsInWindow);
    350         }
    351 
    352         // Calculate the visible area.
    353         var visibleTop = this._containerElement.scrollTop;
    354         var visibleBottom = visibleTop + this._containerElement.clientHeight;
    355 
    356         // Define row height, should be in sync with styles for timeline graphs.
    357         const rowHeight = 18;
    358         const expandOffset = 15;
    359 
    360         // Convert visible area to visible indexes. Always include top-level record for a visible nested record.
    361         var startIndex = Math.max(0, Math.min(Math.floor(visibleTop / rowHeight) - 1, recordsInWindow.length - 1));
    362         while (startIndex > 0 && recordsInWindow[startIndex].parent)
    363             startIndex--;
    364         var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
    365         while (endIndex < recordsInWindow.length - 1 && recordsInWindow[endIndex].parent)
    366             endIndex++;
    367 
    368         // Resize gaps first.
    369         const top = (startIndex * rowHeight) + "px";
    370         this._topGapElement.style.height = top;
    371         this.sidebarElement.style.top = top;
    372         this.sidebarResizeElement.style.top = top;
    373         this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
    374 
    375         // Update visible rows.
    376         var listRowElement = this._sidebarListElement.firstChild;
    377         var width = this._graphRowsElement.offsetWidth;
    378         this._itemsGraphsElement.removeChild(this._graphRowsElement);
    379         var graphRowElement = this._graphRowsElement.firstChild;
    380         var scheduleRefreshCallback = this._scheduleRefresh.bind(this, true);
    381         for (var i = startIndex; i < endIndex; ++i) {
    382             var record = recordsInWindow[i];
    383             var isEven = !(i % 2);
    384 
    385             if (!listRowElement) {
    386                 listRowElement = new WebInspector.TimelineRecordListRow().element;
    387                 this._sidebarListElement.appendChild(listRowElement);
    388             }
    389             if (!graphRowElement) {
    390                 graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, scheduleRefreshCallback, rowHeight).element;
    391                 this._graphRowsElement.appendChild(graphRowElement);
    392             }
    393 
    394             listRowElement.listRow.update(record, isEven);
    395             graphRowElement.graphRow.update(record, isEven, this._calculator, width, expandOffset, i);
    396 
    397             listRowElement = listRowElement.nextSibling;
    398             graphRowElement = graphRowElement.nextSibling;
    399         }
    400 
    401         // Remove extra rows.
    402         while (listRowElement) {
    403             var nextElement = listRowElement.nextSibling;
    404             listRowElement.listRow.dispose();
    405             listRowElement = nextElement;
    406         }
    407         while (graphRowElement) {
    408             var nextElement = graphRowElement.nextSibling;
    409             graphRowElement.graphRow.dispose();
    410             graphRowElement = nextElement;
    411         }
    412 
    413         this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
    414         // Reserve some room for expand / collapse controls to the left for records that start at 0ms.
    415         var timelinePaddingLeft = this._calculator.windowLeft === 0 ? expandOffset : 0;
    416         if (updateBoundaries)
    417             this._timelineGrid.updateDividers(true, this._calculator, timelinePaddingLeft);
    418         this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight);
    419     },
    420 
    421     _addToRecordsWindow: function(record, recordsWindow)
    422     {
    423         recordsWindow.push(record);
    424         if (!record.collapsed) {
    425             var index = recordsWindow.length;
    426             for (var i = 0; record.children && i < record.children.length; ++i)
    427                 this._addToRecordsWindow(record.children[i], recordsWindow);
    428             record.visibleChildrenCount = recordsWindow.length - index;
    429         }
    430     },
    431 
    432     _adjustScrollPosition: function(totalHeight)
    433     {
    434         // Prevent the container from being scrolled off the end.
    435         if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1)
    436             this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
    437     }
    438 }
    439 
    440 WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
    441 
    442 
    443 WebInspector.TimelineCategory = function(name, title, color)
    444 {
    445     this.name = name;
    446     this.title = title;
    447     this.color = color;
    448 }
    449 
    450 
    451 WebInspector.TimelineCalculator = function()
    452 {
    453     this.reset();
    454     this.windowLeft = 0.0;
    455     this.windowRight = 1.0;
    456     this._uiString = WebInspector.UIString.bind(WebInspector);
    457 }
    458 
    459 WebInspector.TimelineCalculator.prototype = {
    460     computeBarGraphPercentages: function(record)
    461     {
    462         var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
    463         var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100;
    464         return {start: start, end: end};
    465     },
    466 
    467     calculateWindow: function()
    468     {
    469         this.minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
    470         this.maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
    471         this.boundarySpan = this.maximumBoundary - this.minimumBoundary;
    472     },
    473 
    474     reset: function()
    475     {
    476         this._absoluteMinimumBoundary = -1;
    477         this._absoluteMaximumBoundary = -1;
    478     },
    479 
    480     updateBoundaries: function(record)
    481     {
    482         var lowerBound = record.startTime;
    483         if (this._absoluteMinimumBoundary === -1 || lowerBound < this._absoluteMinimumBoundary)
    484             this._absoluteMinimumBoundary = lowerBound;
    485 
    486         var upperBound = record.endTime;
    487         if (this._absoluteMaximumBoundary === -1 || upperBound > this._absoluteMaximumBoundary)
    488             this._absoluteMaximumBoundary = upperBound;
    489     },
    490 
    491     formatValue: function(value)
    492     {
    493         return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary, this._uiString);
    494     }
    495 }
    496 
    497 
    498 WebInspector.TimelineRecordListRow = function()
    499 {
    500     this.element = document.createElement("div");
    501     this.element.listRow = this;
    502     var iconElement = document.createElement("span");
    503     iconElement.className = "timeline-tree-icon";
    504     this.element.appendChild(iconElement);
    505 
    506     this._typeElement = document.createElement("span");
    507     this._typeElement.className = "type";
    508     this.element.appendChild(this._typeElement);
    509 
    510     var separatorElement = document.createElement("span");
    511     separatorElement.className = "separator";
    512     separatorElement.textContent = " ";
    513 
    514     this._dataElement = document.createElement("span");
    515     this._dataElement.className = "data dimmed";
    516 
    517     this._repeatCountElement = document.createElement("span");
    518     this._repeatCountElement.className = "count";
    519 
    520     this.element.appendChild(separatorElement);
    521     this.element.appendChild(this._dataElement);
    522     this.element.appendChild(this._repeatCountElement);
    523 }
    524 
    525 WebInspector.TimelineRecordListRow.prototype = {
    526     update: function(record, isEven)
    527     {
    528         this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : "");
    529         this._typeElement.textContent = record.title;
    530 
    531         if (record.details) {
    532             this._dataElement.textContent = "(" + record.details + ")";
    533             this._dataElement.title = record.details;
    534         } else {
    535             this._dataElement.textContent = "";
    536             this._dataElement.title = "";
    537         }
    538 
    539         if (record.count > 1)
    540             this._repeatCountElement.textContent = "\u2009\u00d7\u2009" + record.count;
    541         else
    542             this._repeatCountElement.textContent = "";
    543     },
    544 
    545     dispose: function()
    546     {
    547         this.element.parentElement.removeChild(this.element);
    548     }
    549 }
    550 
    551 
    552 WebInspector.TimelineRecordGraphRow = function(graphContainer, refreshCallback, rowHeight)
    553 {
    554     this.element = document.createElement("div");
    555     this.element.graphRow = this;
    556 
    557     this._barAreaElement = document.createElement("div");
    558     this._barAreaElement.className = "timeline-graph-bar-area";
    559     this.element.appendChild(this._barAreaElement);
    560 
    561     this._barElement = document.createElement("div");
    562     this._barElement.className = "timeline-graph-bar";
    563     this._barAreaElement.appendChild(this._barElement);
    564 
    565     this._expandElement = document.createElement("div");
    566     this._expandElement.className = "timeline-expandable";
    567     graphContainer.appendChild(this._expandElement);
    568 
    569     var leftBorder = document.createElement("div");
    570     leftBorder.className = "timeline-expandable-left";
    571     this._expandElement.appendChild(leftBorder);
    572 
    573     this._expandElement.addEventListener("click", this._onClick.bind(this));
    574     this._refreshCallback = refreshCallback;
    575     this._rowHeight = rowHeight;
    576 }
    577 
    578 WebInspector.TimelineRecordGraphRow.prototype = {
    579     update: function(record, isEven, calculator, clientWidth, expandOffset, index)
    580     {
    581         this._record = record;
    582         this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : "");
    583         var percentages = calculator.computeBarGraphPercentages(record);
    584         var left = percentages.start / 100 * clientWidth;
    585         var width = (percentages.end - percentages.start) / 100 * clientWidth;
    586         this._barElement.style.left = (left + expandOffset) + "px";
    587         this._barElement.style.width = width + "px";
    588 
    589         if (record.visibleChildrenCount) {
    590             this._expandElement.style.top = index * this._rowHeight + "px";
    591             this._expandElement.style.left = left + "px";
    592             this._expandElement.style.width = Math.max(12, width + 25) + "px";
    593             if (!record.collapsed) {
    594                 this._expandElement.style.height = (record.visibleChildrenCount + 1) * this._rowHeight + "px";
    595                 this._expandElement.addStyleClass("timeline-expandable-expanded");
    596                 this._expandElement.removeStyleClass("timeline-expandable-collapsed");
    597             } else {
    598                 this._expandElement.style.height = this._rowHeight + "px";
    599                 this._expandElement.addStyleClass("timeline-expandable-collapsed");
    600                 this._expandElement.removeStyleClass("timeline-expandable-expanded");
    601             }
    602             this._expandElement.removeStyleClass("hidden");
    603         } else {
    604             this._expandElement.addStyleClass("hidden");
    605         }
    606     },
    607 
    608     _onClick: function(event)
    609     {
    610         this._record.collapsed = !this._record.collapsed;
    611         this._refreshCallback();
    612     },
    613 
    614     dispose: function()
    615     {
    616         this.element.parentElement.removeChild(this.element);
    617         this._expandElement.parentElement.removeChild(this._expandElement);
    618     }
    619 }
    620