/* * Copyright (C) 2009 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ WebInspector.TimelinePanel = function() { WebInspector.Panel.call(this, "timeline"); this.element.appendChild(this._createTopPane()); this.element.tabIndex = 0; this._sidebarBackgroundElement = document.createElement("div"); this._sidebarBackgroundElement.className = "sidebar timeline-sidebar-background"; this.element.appendChild(this._sidebarBackgroundElement); this._containerElement = document.createElement("div"); this._containerElement.id = "timeline-container"; this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false); this.element.appendChild(this._containerElement); this.createSidebar(this._containerElement, this._containerElement); var itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true); itemsTreeElement.expanded = true; this.sidebarTree.appendChild(itemsTreeElement); this._sidebarListElement = document.createElement("div"); this.sidebarElement.appendChild(this._sidebarListElement); this._containerContentElement = document.createElement("div"); this._containerContentElement.id = "resources-container-content"; this._containerElement.appendChild(this._containerContentElement); this._timelineGrid = new WebInspector.TimelineGrid(); this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement; this._itemsGraphsElement.id = "timeline-graphs"; this._containerContentElement.appendChild(this._timelineGrid.element); this._topGapElement = document.createElement("div"); this._topGapElement.className = "timeline-gap"; this._itemsGraphsElement.appendChild(this._topGapElement); this._graphRowsElement = document.createElement("div"); this._itemsGraphsElement.appendChild(this._graphRowsElement); this._bottomGapElement = document.createElement("div"); this._bottomGapElement.className = "timeline-gap"; this._itemsGraphsElement.appendChild(this._bottomGapElement); this._expandElements = document.createElement("div"); this._expandElements.id = "orphan-expand-elements"; this._itemsGraphsElement.appendChild(this._expandElements); this._rootRecord = this._createRootRecord(); this._sendRequestRecords = {}; this._scheduledResourceRequests = {}; this._timerRecords = {}; this._calculator = new WebInspector.TimelineCalculator(); this._calculator._showShortEvents = false; var shortRecordThresholdTitle = Number.secondsToString(WebInspector.TimelinePanel.shortRecordThreshold); this._showShortRecordsTitleText = WebInspector.UIString("Show the records that are shorter than %s", shortRecordThresholdTitle); this._hideShortRecordsTitleText = WebInspector.UIString("Hide the records that are shorter than %s", shortRecordThresholdTitle); this._createStatusbarButtons(); this._boundariesAreValid = true; this._scrollTop = 0; this._popoverHelper = new WebInspector.PopoverHelper(this._containerElement, this._getPopoverAnchor.bind(this), this._showPopover.bind(this), true); // Disable short events filter by default. this.toggleFilterButton.toggled = true; this._calculator._showShortEvents = this.toggleFilterButton.toggled; this._markTimelineRecords = []; this._expandOffset = 15; InspectorBackend.registerDomainDispatcher("Timeline", new WebInspector.TimelineDispatcher(this)); } // Define row height, should be in sync with styles for timeline graphs. WebInspector.TimelinePanel.rowHeight = 18; WebInspector.TimelinePanel.shortRecordThreshold = 0.015; WebInspector.TimelinePanel.prototype = { _createTopPane: function() { var topPaneElement = document.createElement("div"); topPaneElement.id = "timeline-overview-panel"; this._topPaneSidebarElement = document.createElement("div"); this._topPaneSidebarElement.id = "timeline-overview-sidebar"; var overviewTreeElement = document.createElement("ol"); overviewTreeElement.className = "sidebar-tree"; this._topPaneSidebarElement.appendChild(overviewTreeElement); topPaneElement.appendChild(this._topPaneSidebarElement); var topPaneSidebarTree = new TreeOutline(overviewTreeElement); var timelinesOverviewItem = new WebInspector.SidebarTreeElement("resources-time-graph-sidebar-item", WebInspector.UIString("Timelines")); topPaneSidebarTree.appendChild(timelinesOverviewItem); timelinesOverviewItem.onselect = this._timelinesOverviewItemSelected.bind(this); timelinesOverviewItem.select(true); var memoryOverviewItem = new WebInspector.SidebarTreeElement("resources-size-graph-sidebar-item", WebInspector.UIString("Memory")); topPaneSidebarTree.appendChild(memoryOverviewItem); memoryOverviewItem.onselect = this._memoryOverviewItemSelected.bind(this); this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories); this._overviewPane.addEventListener("window changed", this._windowChanged, this); this._overviewPane.addEventListener("filter changed", this._refresh, this); topPaneElement.appendChild(this._overviewPane.element); var separatorElement = document.createElement("div"); separatorElement.id = "timeline-overview-separator"; topPaneElement.appendChild(separatorElement); return topPaneElement; }, get toolbarItemLabel() { return WebInspector.UIString("Timeline"); }, get statusBarItems() { return [this.toggleFilterButton.element, this.toggleTimelineButton.element, this.garbageCollectButton.element, this.clearButton.element, this._overviewPane.statusBarFilters]; }, get categories() { if (!this._categories) { this._categories = { loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"), scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"), rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)") }; } return this._categories; }, get defaultFocusedElement() { return this.element; }, get _recordStyles() { if (!this._recordStylesArray) { var recordTypes = WebInspector.TimelineAgent.RecordType; var recordStyles = {}; recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting }; recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering }; recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering }; recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering }; recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading }; recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting }; recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting }; recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting }; recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting }; recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting }; recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting }; recordStyles[recordTypes.MarkTimeline] = { title: WebInspector.UIString("Mark"), category: this.categories.scripting }; recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading }; recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading }; recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading }; recordStyles[recordTypes.FunctionCall] = { title: WebInspector.UIString("Function Call"), category: this.categories.scripting }; recordStyles[recordTypes.ResourceReceivedData] = { title: WebInspector.UIString("Receive Data"), category: this.categories.loading }; recordStyles[recordTypes.GCEvent] = { title: WebInspector.UIString("GC Event"), category: this.categories.scripting }; recordStyles[recordTypes.MarkDOMContent] = { title: WebInspector.UIString("DOMContent event"), category: this.categories.scripting }; recordStyles[recordTypes.MarkLoad] = { title: WebInspector.UIString("Load event"), category: this.categories.scripting }; recordStyles[recordTypes.ScheduleResourceRequest] = { title: WebInspector.UIString("Schedule Request"), category: this.categories.loading }; this._recordStylesArray = recordStyles; } return this._recordStylesArray; }, _createStatusbarButtons: function() { this.toggleTimelineButton = new WebInspector.StatusBarButton(WebInspector.UIString("Record"), "record-profile-status-bar-item"); this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false); this.clearButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear"), "clear-status-bar-item"); this.clearButton.addEventListener("click", this._clearPanel.bind(this), false); this.toggleFilterButton = new WebInspector.StatusBarButton(this._hideShortRecordsTitleText, "timeline-filter-status-bar-item"); this.toggleFilterButton.addEventListener("click", this._toggleFilterButtonClicked.bind(this), false); this.garbageCollectButton = new WebInspector.StatusBarButton(WebInspector.UIString("Collect Garbage"), "garbage-collect-status-bar-item"); this.garbageCollectButton.addEventListener("click", this._garbageCollectButtonClicked.bind(this), false); this.recordsCounter = document.createElement("span"); this.recordsCounter.className = "timeline-records-counter"; }, _updateRecordsCounter: function() { this.recordsCounter.textContent = WebInspector.UIString("%d of %d captured records are visible", this._rootRecord._visibleRecordsCount, this._rootRecord._allRecordsCount); }, _updateEventDividers: function() { this._timelineGrid.removeEventDividers(); var clientWidth = this._graphRowsElement.offsetWidth - this._expandOffset; var dividers = []; for (var i = 0; i < this._markTimelineRecords.length; ++i) { var record = this._markTimelineRecords[i]; var positions = this._calculator.computeBarGraphWindowPosition(record, clientWidth); var dividerPosition = Math.round(positions.left); if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition]) continue; var divider = this._createEventDivider(record); divider.style.left = (dividerPosition + this._expandOffset) + "px"; dividers[dividerPosition] = divider; } this._timelineGrid.addEventDividers(dividers); this._overviewPane.updateEventDividers(this._markTimelineRecords, this._createEventDivider.bind(this)); }, _createEventDivider: function(record) { var eventDivider = document.createElement("div"); eventDivider.className = "resources-event-divider"; var recordTypes = WebInspector.TimelineAgent.RecordType; var eventDividerPadding = document.createElement("div"); eventDividerPadding.className = "resources-event-divider-padding"; eventDividerPadding.title = record.title; if (record.type === recordTypes.MarkDOMContent) eventDivider.className += " resources-blue-divider"; else if (record.type === recordTypes.MarkLoad) eventDivider.className += " resources-red-divider"; else if (record.type === recordTypes.MarkTimeline) { eventDivider.className += " resources-orange-divider"; eventDividerPadding.title = record.data.message; } eventDividerPadding.appendChild(eventDivider); return eventDividerPadding; }, _timelinesOverviewItemSelected: function(event) { this._overviewPane.showTimelines(); }, _memoryOverviewItemSelected: function(event) { this._overviewPane.showMemoryGraph(this._rootRecord.children); }, _toggleTimelineButtonClicked: function() { if (this.toggleTimelineButton.toggled) TimelineAgent.stop(); else { this._clearPanel(); TimelineAgent.start(); } }, _toggleFilterButtonClicked: function() { this.toggleFilterButton.toggled = !this.toggleFilterButton.toggled; this._calculator._showShortEvents = this.toggleFilterButton.toggled; this.toggleFilterButton.element.title = this._calculator._showShortEvents ? this._hideShortRecordsTitleText : this._showShortRecordsTitleText; this._scheduleRefresh(true); }, _garbageCollectButtonClicked: function() { ProfilerAgent.collectGarbage(); }, _timelineProfilerWasStarted: function() { this.toggleTimelineButton.toggled = true; }, _timelineProfilerWasStopped: function() { this.toggleTimelineButton.toggled = false; }, _addRecordToTimeline: function(record) { if (record.type == WebInspector.TimelineAgent.RecordType.ResourceSendRequest) { var isMainResource = (record.data.identifier === WebInspector.mainResource.identifier); if (isMainResource && this._mainResourceIdentifier !== record.data.identifier) { // We are loading new main resource -> clear the panel. Check above is necessary since // there may be several resource loads with main resource marker upon redirects, redirects are reported with // the original identifier. this._mainResourceIdentifier = record.data.identifier; this._clearPanel(); } } this._innerAddRecordToTimeline(record, this._rootRecord); this._scheduleRefresh(); }, _findParentRecord: function(record) { var recordTypes = WebInspector.TimelineAgent.RecordType; var parentRecord; if (record.type === recordTypes.ResourceReceiveResponse || record.type === recordTypes.ResourceFinish || record.type === recordTypes.ResourceReceivedData) parentRecord = this._sendRequestRecords[record.data.identifier]; else if (record.type === recordTypes.TimerFire) parentRecord = this._timerRecords[record.data.timerId]; else if (record.type === recordTypes.ResourceSendRequest) parentRecord = this._scheduledResourceRequests[record.data.url]; return parentRecord; }, _innerAddRecordToTimeline: function(record, parentRecord) { var connectedToOldRecord = false; var recordTypes = WebInspector.TimelineAgent.RecordType; if (record.type === recordTypes.MarkDOMContent || record.type === recordTypes.MarkLoad) parentRecord = null; // No bar entry for load events. else if (parentRecord === this._rootRecord) { var newParentRecord = this._findParentRecord(record); if (newParentRecord) { parentRecord = newParentRecord; connectedToOldRecord = true; } } if (record.type == recordTypes.TimerFire && record.children && record.children.length) { var childRecord = record.children[0]; if (childRecord.type === recordTypes.FunctionCall) { record.data.scriptName = childRecord.data.scriptName; record.data.scriptLine = childRecord.data.scriptLine; record.children.shift(); record.children = childRecord.children.concat(record.children); } } var formattedRecord = new WebInspector.TimelinePanel.FormattedRecord(record, parentRecord, this); if (record.type === recordTypes.MarkDOMContent || record.type === recordTypes.MarkLoad) { this._markTimelineRecords.push(formattedRecord); return; } ++this._rootRecord._allRecordsCount; formattedRecord.collapsed = (parentRecord === this._rootRecord); var childrenCount = record.children ? record.children.length : 0; for (var i = 0; i < childrenCount; ++i) this._innerAddRecordToTimeline(record.children[i], formattedRecord); formattedRecord._calculateAggregatedStats(this.categories); if (connectedToOldRecord) { var record = formattedRecord; do { var parent = record.parent; parent._cpuTime += formattedRecord._cpuTime; if (parent._lastChildEndTime < record._lastChildEndTime) parent._lastChildEndTime = record._lastChildEndTime; for (var category in formattedRecord._aggregatedStats) parent._aggregatedStats[category] += formattedRecord._aggregatedStats[category]; record = parent; } while (record.parent); } else if (parentRecord !== this._rootRecord) parentRecord._selfTime -= formattedRecord.endTime - formattedRecord.startTime; // Keep bar entry for mark timeline since nesting might be interesting to the user. if (record.type === recordTypes.MarkTimeline) this._markTimelineRecords.push(formattedRecord); }, setSidebarWidth: function(width) { WebInspector.Panel.prototype.setSidebarWidth.call(this, width); this._sidebarBackgroundElement.style.width = width + "px"; this._topPaneSidebarElement.style.width = width + "px"; }, updateMainViewWidth: function(width) { this._containerContentElement.style.left = width + "px"; this._scheduleRefresh(); this._overviewPane.updateMainViewWidth(width); }, resize: function() { this._closeRecordDetails(); this._scheduleRefresh(); }, _createRootRecord: function() { var rootRecord = {}; rootRecord.children = []; rootRecord._visibleRecordsCount = 0; rootRecord._allRecordsCount = 0; rootRecord._aggregatedStats = {}; return rootRecord; }, _clearPanel: function() { this._markTimelineRecords = []; this._sendRequestRecords = {}; this._scheduledResourceRequests = {}; this._timerRecords = {}; this._rootRecord = this._createRootRecord(); this._boundariesAreValid = false; this._overviewPane.reset(); this._adjustScrollPosition(0); this._refresh(); this._closeRecordDetails(); }, show: function() { WebInspector.Panel.prototype.show.call(this); if (typeof this._scrollTop === "number") this._containerElement.scrollTop = this._scrollTop; this._refresh(); WebInspector.drawer.currentPanelCounters = this.recordsCounter; }, hide: function() { WebInspector.Panel.prototype.hide.call(this); this._closeRecordDetails(); WebInspector.drawer.currentPanelCounters = null; }, _onScroll: function(event) { this._closeRecordDetails(); var scrollTop = this._containerElement.scrollTop; var dividersTop = Math.max(0, scrollTop); this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop); this._scheduleRefresh(true); }, _windowChanged: function() { this._closeRecordDetails(); this._scheduleRefresh(); }, _scheduleRefresh: function(preserveBoundaries) { this._closeRecordDetails(); this._boundariesAreValid &= preserveBoundaries; if (!this.visible) return; if (preserveBoundaries) this._refresh(); else if (!this._refreshTimeout) this._refreshTimeout = setTimeout(this._refresh.bind(this), 100); }, _refresh: function() { if (this._refreshTimeout) { clearTimeout(this._refreshTimeout); delete this._refreshTimeout; } this._overviewPane.update(this._rootRecord.children, this._calculator._showShortEvents); this._refreshRecords(!this._boundariesAreValid); this._updateRecordsCounter(); if(!this._boundariesAreValid) this._updateEventDividers(); this._boundariesAreValid = true; }, _updateBoundaries: function() { this._calculator.reset(); this._calculator.windowLeft = this._overviewPane.windowLeft; this._calculator.windowRight = this._overviewPane.windowRight; for (var i = 0; i < this._rootRecord.children.length; ++i) this._calculator.updateBoundaries(this._rootRecord.children[i]); this._calculator.calculateWindow(); }, _addToRecordsWindow: function(record, recordsWindow, parentIsCollapsed) { if (!this._calculator._showShortEvents && !record.isLong()) return; var percentages = this._calculator.computeBarGraphPercentages(record); if (percentages.start < 100 && percentages.endWithChildren >= 0 && !record.category.hidden) { ++this._rootRecord._visibleRecordsCount; ++record.parent._invisibleChildrenCount; if (!parentIsCollapsed) recordsWindow.push(record); } var index = recordsWindow.length; record._invisibleChildrenCount = 0; for (var i = 0; i < record.children.length; ++i) this._addToRecordsWindow(record.children[i], recordsWindow, parentIsCollapsed || record.collapsed); record._visibleChildrenCount = recordsWindow.length - index; }, _filterRecords: function() { var recordsInWindow = []; this._rootRecord._visibleRecordsCount = 0; for (var i = 0; i < this._rootRecord.children.length; ++i) this._addToRecordsWindow(this._rootRecord.children[i], recordsInWindow); return recordsInWindow; }, _refreshRecords: function(updateBoundaries) { if (updateBoundaries) this._updateBoundaries(); var recordsInWindow = this._filterRecords(); // Calculate the visible area. this._scrollTop = this._containerElement.scrollTop; var visibleTop = this._scrollTop; var visibleBottom = visibleTop + this._containerElement.clientHeight; const rowHeight = WebInspector.TimelinePanel.rowHeight; // Convert visible area to visible indexes. Always include top-level record for a visible nested record. var startIndex = Math.max(0, Math.min(Math.floor(visibleTop / rowHeight) - 1, recordsInWindow.length - 1)); var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight)); // Resize gaps first. const top = (startIndex * rowHeight) + "px"; this._topGapElement.style.height = top; this.sidebarElement.style.top = top; this.sidebarResizeElement.style.top = top; this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px"; // Update visible rows. var listRowElement = this._sidebarListElement.firstChild; var width = this._graphRowsElement.offsetWidth; this._itemsGraphsElement.removeChild(this._graphRowsElement); var graphRowElement = this._graphRowsElement.firstChild; var scheduleRefreshCallback = this._scheduleRefresh.bind(this, true); this._itemsGraphsElement.removeChild(this._expandElements); this._expandElements.removeChildren(); for (var i = 0; i < endIndex; ++i) { var record = recordsInWindow[i]; var isEven = !(i % 2); if (i < startIndex) { var lastChildIndex = i + record._visibleChildrenCount; if (lastChildIndex >= startIndex && lastChildIndex < endIndex) { var expandElement = new WebInspector.TimelineExpandableElement(this._expandElements); expandElement._update(record, i, this._calculator.computeBarGraphWindowPosition(record, width - this._expandOffset)); } } else { if (!listRowElement) { listRowElement = new WebInspector.TimelineRecordListRow().element; this._sidebarListElement.appendChild(listRowElement); } if (!graphRowElement) { graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, scheduleRefreshCallback, rowHeight).element; this._graphRowsElement.appendChild(graphRowElement); } listRowElement.row.update(record, isEven, this._calculator, visibleTop); graphRowElement.row.update(record, isEven, this._calculator, width, this._expandOffset, i); listRowElement = listRowElement.nextSibling; graphRowElement = graphRowElement.nextSibling; } } // Remove extra rows. while (listRowElement) { var nextElement = listRowElement.nextSibling; listRowElement.row.dispose(); listRowElement = nextElement; } while (graphRowElement) { var nextElement = graphRowElement.nextSibling; graphRowElement.row.dispose(); graphRowElement = nextElement; } this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement); this._itemsGraphsElement.appendChild(this._expandElements); this.sidebarResizeElement.style.height = this.sidebarElement.clientHeight + "px"; // Reserve some room for expand / collapse controls to the left for records that start at 0ms. var timelinePaddingLeft = this._calculator.windowLeft === 0 ? this._expandOffset : 0; if (updateBoundaries) this._timelineGrid.updateDividers(true, this._calculator, timelinePaddingLeft); this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight); }, _adjustScrollPosition: function(totalHeight) { // Prevent the container from being scrolled off the end. if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1) this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight); }, _getPopoverAnchor: function(element) { return element.enclosingNodeOrSelfWithClass("timeline-graph-bar") || element.enclosingNodeOrSelfWithClass("timeline-tree-item"); }, _showPopover: function(anchor) { var record = anchor.row._record; var popover = new WebInspector.Popover(record._generatePopupContent(this._calculator, this.categories)); popover.show(anchor); return popover; }, _closeRecordDetails: function() { this._popoverHelper.hidePopup(); } } WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype; WebInspector.TimelineDispatcher = function(timelinePanel) { this._timelinePanel = timelinePanel; } WebInspector.TimelineDispatcher.prototype = { started: function() { this._timelinePanel._timelineProfilerWasStarted(); }, stopped: function() { this._timelinePanel._timelineProfilerWasStopped(); }, eventRecorded: function(record) { this._timelinePanel._addRecordToTimeline(record); } } WebInspector.TimelineCategory = function(name, title, color) { this.name = name; this.title = title; this.color = color; } WebInspector.TimelineCalculator = function() { this.reset(); this.windowLeft = 0.0; this.windowRight = 1.0; } WebInspector.TimelineCalculator.prototype = { computeBarGraphPercentages: function(record) { var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100; var end = (record.startTime + record._selfTime - this.minimumBoundary) / this.boundarySpan * 100; var endWithChildren = (record._lastChildEndTime - this.minimumBoundary) / this.boundarySpan * 100; var cpuWidth = record._cpuTime / this.boundarySpan * 100; return {start: start, end: end, endWithChildren: endWithChildren, cpuWidth: cpuWidth}; }, computeBarGraphWindowPosition: function(record, clientWidth) { const minWidth = 5; const borderWidth = 4; var workingArea = clientWidth - minWidth - borderWidth; var percentages = this.computeBarGraphPercentages(record); var left = percentages.start / 100 * workingArea; var width = (percentages.end - percentages.start) / 100 * workingArea + minWidth; var widthWithChildren = (percentages.endWithChildren - percentages.start) / 100 * workingArea; var cpuWidth = percentages.cpuWidth / 100 * workingArea + minWidth; if (percentages.endWithChildren > percentages.end) widthWithChildren += borderWidth + minWidth; return {left: left, width: width, widthWithChildren: widthWithChildren, cpuWidth: cpuWidth}; }, calculateWindow: function() { this.minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary); this.maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary); this.boundarySpan = this.maximumBoundary - this.minimumBoundary; }, reset: function() { this._absoluteMinimumBoundary = -1; this._absoluteMaximumBoundary = -1; }, updateBoundaries: function(record) { var lowerBound = record.startTime; if (this._absoluteMinimumBoundary === -1 || lowerBound < this._absoluteMinimumBoundary) this._absoluteMinimumBoundary = lowerBound; const minimumTimeFrame = 0.1; const minimumDeltaForZeroSizeEvents = 0.01; var upperBound = Math.max(record._lastChildEndTime + minimumDeltaForZeroSizeEvents, lowerBound + minimumTimeFrame); if (this._absoluteMaximumBoundary === -1 || upperBound > this._absoluteMaximumBoundary) this._absoluteMaximumBoundary = upperBound; }, formatValue: function(value) { return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary); } } WebInspector.TimelineRecordListRow = function() { this.element = document.createElement("div"); this.element.row = this; this.element.style.cursor = "pointer"; var iconElement = document.createElement("span"); iconElement.className = "timeline-tree-icon"; this.element.appendChild(iconElement); this._typeElement = document.createElement("span"); this._typeElement.className = "type"; this.element.appendChild(this._typeElement); var separatorElement = document.createElement("span"); separatorElement.className = "separator"; separatorElement.textContent = " "; this._dataElement = document.createElement("span"); this._dataElement.className = "data dimmed"; this.element.appendChild(separatorElement); this.element.appendChild(this._dataElement); } WebInspector.TimelineRecordListRow.prototype = { update: function(record, isEven, calculator, offset) { this._record = record; this._calculator = calculator; this._offset = offset; this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : ""); this._typeElement.textContent = record.title; if (this._dataElement.firstChild) this._dataElement.removeChildren(); if (record.details) { var detailsContainer = document.createElement("span"); if (typeof record.details === "object") { detailsContainer.appendChild(document.createTextNode("(")); detailsContainer.appendChild(record.details); detailsContainer.appendChild(document.createTextNode(")")); } else detailsContainer.textContent = "(" + record.details + ")"; this._dataElement.appendChild(detailsContainer); } }, dispose: function() { this.element.parentElement.removeChild(this.element); } } WebInspector.TimelineRecordGraphRow = function(graphContainer, scheduleRefresh) { this.element = document.createElement("div"); this.element.row = this; this._barAreaElement = document.createElement("div"); this._barAreaElement.className = "timeline-graph-bar-area"; this.element.appendChild(this._barAreaElement); this._barWithChildrenElement = document.createElement("div"); this._barWithChildrenElement.className = "timeline-graph-bar with-children"; this._barWithChildrenElement.row = this; this._barAreaElement.appendChild(this._barWithChildrenElement); this._barCpuElement = document.createElement("div"); this._barCpuElement.className = "timeline-graph-bar cpu" this._barCpuElement.row = this; this._barAreaElement.appendChild(this._barCpuElement); this._barElement = document.createElement("div"); this._barElement.className = "timeline-graph-bar"; this._barElement.row = this; this._barAreaElement.appendChild(this._barElement); this._expandElement = new WebInspector.TimelineExpandableElement(graphContainer); this._expandElement._element.addEventListener("click", this._onClick.bind(this)); this._scheduleRefresh = scheduleRefresh; } WebInspector.TimelineRecordGraphRow.prototype = { update: function(record, isEven, calculator, clientWidth, expandOffset, index) { this._record = record; this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : ""); var barPosition = calculator.computeBarGraphWindowPosition(record, clientWidth - expandOffset); this._barWithChildrenElement.style.left = barPosition.left + expandOffset + "px"; this._barWithChildrenElement.style.width = barPosition.widthWithChildren + "px"; this._barElement.style.left = barPosition.left + expandOffset + "px"; this._barElement.style.width = barPosition.width + "px"; this._barCpuElement.style.left = barPosition.left + expandOffset + "px"; this._barCpuElement.style.width = barPosition.cpuWidth + "px"; this._expandElement._update(record, index, barPosition); }, _onClick: function(event) { this._record.collapsed = !this._record.collapsed; this._scheduleRefresh(); }, dispose: function() { this.element.parentElement.removeChild(this.element); this._expandElement._dispose(); } } WebInspector.TimelinePanel.FormattedRecord = function(record, parentRecord, panel) { var recordTypes = WebInspector.TimelineAgent.RecordType; var style = panel._recordStyles[record.type]; this.parent = parentRecord; if (parentRecord) parentRecord.children.push(this); this.category = style.category; this.title = style.title; this.startTime = record.startTime / 1000; this.data = record.data; this.type = record.type; this.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : this.startTime; this._selfTime = this.endTime - this.startTime; this._lastChildEndTime = this.endTime; if (record.stackTrace && record.stackTrace.length) this.stackTrace = record.stackTrace; this.totalHeapSize = record.totalHeapSize; this.usedHeapSize = record.usedHeapSize; // Make resource receive record last since request was sent; make finish record last since response received. if (record.type === recordTypes.ResourceSendRequest) { panel._sendRequestRecords[record.data.identifier] = this; } else if (record.type === recordTypes.ScheduleResourceRequest) { panel._scheduledResourceRequests[record.data.url] = this; } else if (record.type === recordTypes.ResourceReceiveResponse) { var sendRequestRecord = panel._sendRequestRecords[record.data.identifier]; if (sendRequestRecord) { // False if we started instrumentation in the middle of request. record.data.url = sendRequestRecord.data.url; // Now that we have resource in the collection, recalculate details in order to display short url. sendRequestRecord.details = this._getRecordDetails(sendRequestRecord, panel._sendRequestRecords); if (sendRequestRecord.parent !== panel._rootRecord && sendRequestRecord.parent.type === recordTypes.ScheduleResourceRequest) sendRequestRecord.parent.details = this._getRecordDetails(sendRequestRecord, panel._sendRequestRecords); } } else if (record.type === recordTypes.ResourceReceivedData) { var sendRequestRecord = panel._sendRequestRecords[record.data.identifier]; if (sendRequestRecord) // False for main resource. record.data.url = sendRequestRecord.data.url; } else if (record.type === recordTypes.ResourceFinish) { var sendRequestRecord = panel._sendRequestRecords[record.data.identifier]; if (sendRequestRecord) // False for main resource. record.data.url = sendRequestRecord.data.url; } else if (record.type === recordTypes.TimerInstall) { this.timeout = record.data.timeout; this.singleShot = record.data.singleShot; panel._timerRecords[record.data.timerId] = this; } else if (record.type === recordTypes.TimerFire) { var timerInstalledRecord = panel._timerRecords[record.data.timerId]; if (timerInstalledRecord) { this.callSiteStackTrace = timerInstalledRecord.stackTrace; this.timeout = timerInstalledRecord.timeout; this.singleShot = timerInstalledRecord.singleShot; } } this.details = this._getRecordDetails(record, panel._sendRequestRecords); } WebInspector.TimelinePanel.FormattedRecord.prototype = { isLong: function() { return (this._lastChildEndTime - this.startTime) > WebInspector.TimelinePanel.shortRecordThreshold; }, get children() { if (!this._children) this._children = []; return this._children; }, _generateAggregatedInfo: function() { var cell = document.createElement("span"); cell.className = "timeline-aggregated-info"; for (var index in this._aggregatedStats) { var label = document.createElement("div"); label.className = "timeline-aggregated-category timeline-" + index; cell.appendChild(label); var text = document.createElement("span"); text.textContent = Number.secondsToString(this._aggregatedStats[index] + 0.0001); cell.appendChild(text); } return cell; }, _generatePopupContent: function(calculator, categories) { var contentHelper = new WebInspector.TimelinePanel.PopupContentHelper(this.title); if (this._children && this._children.length) { contentHelper._appendTextRow(WebInspector.UIString("Self Time"), Number.secondsToString(this._selfTime + 0.0001)); contentHelper._appendElementRow(WebInspector.UIString("Aggregated Time"), this._generateAggregatedInfo()); } var text = WebInspector.UIString("%s (at %s)", Number.secondsToString(this._lastChildEndTime - this.startTime), calculator.formatValue(this.startTime - calculator.minimumBoundary)); contentHelper._appendTextRow(WebInspector.UIString("Duration"), text); const recordTypes = WebInspector.TimelineAgent.RecordType; switch (this.type) { case recordTypes.GCEvent: contentHelper._appendTextRow(WebInspector.UIString("Collected"), Number.bytesToString(this.data.usedHeapSizeDelta)); break; case recordTypes.TimerInstall: case recordTypes.TimerFire: case recordTypes.TimerRemove: contentHelper._appendTextRow(WebInspector.UIString("Timer ID"), this.data.timerId); if (typeof this.timeout === "number") { contentHelper._appendTextRow(WebInspector.UIString("Timeout"), Number.secondsToString(this.timeout / 1000)); contentHelper._appendTextRow(WebInspector.UIString("Repeats"), !this.singleShot); } break; case recordTypes.FunctionCall: contentHelper._appendLinkRow(WebInspector.UIString("Location"), this.data.scriptName, this.data.scriptLine); break; case recordTypes.ScheduleResourceRequest: case recordTypes.ResourceSendRequest: case recordTypes.ResourceReceiveResponse: case recordTypes.ResourceReceivedData: case recordTypes.ResourceFinish: contentHelper._appendLinkRow(WebInspector.UIString("Resource"), this.data.url); if (this.data.requestMethod) contentHelper._appendTextRow(WebInspector.UIString("Request Method"), this.data.requestMethod); if (typeof this.data.statusCode === "number") contentHelper._appendTextRow(WebInspector.UIString("Status Code"), this.data.statusCode); if (this.data.mimeType) contentHelper._appendTextRow(WebInspector.UIString("MIME Type"), this.data.mimeType); break; case recordTypes.EvaluateScript: if (this.data && this.data.url) contentHelper._appendLinkRow(WebInspector.UIString("Script"), this.data.url, this.data.lineNumber); break; case recordTypes.Paint: contentHelper._appendTextRow(WebInspector.UIString("Location"), WebInspector.UIString("(%d, %d)", this.data.x, this.data.y)); contentHelper._appendTextRow(WebInspector.UIString("Dimensions"), WebInspector.UIString("%d × %d", this.data.width, this.data.height)); case recordTypes.RecalculateStyles: // We don't want to see default details. break; default: if (this.details) contentHelper._appendTextRow(WebInspector.UIString("Details"), this.details); break; } if (this.data.scriptName && this.type !== recordTypes.FunctionCall) contentHelper._appendLinkRow(WebInspector.UIString("Function Call"), this.data.scriptName, this.data.scriptLine); if (this.usedHeapSize) contentHelper._appendTextRow(WebInspector.UIString("Used Heap Size"), WebInspector.UIString("%s of %s", Number.bytesToString(this.usedHeapSize), Number.bytesToString(this.totalHeapSize))); if (this.callSiteStackTrace && this.callSiteStackTrace.length) contentHelper._appendStackTrace(WebInspector.UIString("Call Site stack"), this.callSiteStackTrace); if (this.stackTrace) contentHelper._appendStackTrace(WebInspector.UIString("Call Stack"), this.stackTrace); return contentHelper._contentTable; }, _getRecordDetails: function(record, sendRequestRecords) { switch (record.type) { case WebInspector.TimelineAgent.RecordType.GCEvent: return WebInspector.UIString("%s collected", Number.bytesToString(record.data.usedHeapSizeDelta)); case WebInspector.TimelineAgent.RecordType.TimerFire: return record.data.scriptName ? WebInspector.linkifyResourceAsNode(record.data.scriptName, "scripts", record.data.scriptLine, "", "") : record.data.timerId; case WebInspector.TimelineAgent.RecordType.FunctionCall: return record.data.scriptName ? WebInspector.linkifyResourceAsNode(record.data.scriptName, "scripts", record.data.scriptLine, "", "") : null; case WebInspector.TimelineAgent.RecordType.EventDispatch: return record.data ? record.data.type : null; case WebInspector.TimelineAgent.RecordType.Paint: return record.data.width + "\u2009\u00d7\u2009" + record.data.height; case WebInspector.TimelineAgent.RecordType.TimerInstall: case WebInspector.TimelineAgent.RecordType.TimerRemove: return this.stackTrace ? WebInspector.linkifyResourceAsNode(this.stackTrace[0].scriptName, "scripts", this.stackTrace[0].lineNumber, "", "") : record.data.timerId; case WebInspector.TimelineAgent.RecordType.ParseHTML: case WebInspector.TimelineAgent.RecordType.RecalculateStyles: return this.stackTrace ? WebInspector.linkifyResourceAsNode(this.stackTrace[0].scriptName, "scripts", this.stackTrace[0].lineNumber, "", "") : null; case WebInspector.TimelineAgent.RecordType.EvaluateScript: return record.data.url ? WebInspector.linkifyResourceAsNode(record.data.url, "scripts", record.data.lineNumber, "", "") : null; case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange: case WebInspector.TimelineAgent.RecordType.XHRLoad: case WebInspector.TimelineAgent.RecordType.ScheduleResourceRequest: case WebInspector.TimelineAgent.RecordType.ResourceSendRequest: case WebInspector.TimelineAgent.RecordType.ResourceReceivedData: case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse: case WebInspector.TimelineAgent.RecordType.ResourceFinish: return WebInspector.displayNameForURL(record.data.url); case WebInspector.TimelineAgent.RecordType.MarkTimeline: return record.data.message; default: return null; } }, _calculateAggregatedStats: function(categories) { this._aggregatedStats = {}; for (var category in categories) this._aggregatedStats[category] = 0; this._cpuTime = this._selfTime; if (this._children) { for (var index = this._children.length; index; --index) { var child = this._children[index - 1]; this._aggregatedStats[child.category.name] += child._selfTime; for (var category in categories) this._aggregatedStats[category] += child._aggregatedStats[category]; } for (var category in this._aggregatedStats) this._cpuTime += this._aggregatedStats[category]; } } } WebInspector.TimelinePanel.PopupContentHelper = function(title) { this._contentTable = document.createElement("table");; var titleCell = this._createCell(WebInspector.UIString("%s - Details", title), "timeline-details-title"); titleCell.colSpan = 2; var titleRow = document.createElement("tr"); titleRow.appendChild(titleCell); this._contentTable.appendChild(titleRow); } WebInspector.TimelinePanel.PopupContentHelper.prototype = { _createCell: function(content, styleName) { var text = document.createElement("label"); text.appendChild(document.createTextNode(content)); var cell = document.createElement("td"); cell.className = "timeline-details"; if (styleName) cell.className += " " + styleName; cell.textContent = content; return cell; }, _appendTextRow: function(title, content) { var row = document.createElement("tr"); row.appendChild(this._createCell(title, "timeline-details-row-title")); row.appendChild(this._createCell(content, "timeline-details-row-data")); this._contentTable.appendChild(row); }, _appendElementRow: function(title, content, titleStyle) { var row = document.createElement("tr"); var titleCell = this._createCell(title, "timeline-details-row-title"); if (titleStyle) titleCell.addStyleClass(titleStyle); row.appendChild(titleCell); var cell = document.createElement("td"); cell.className = "timeline-details"; cell.appendChild(content); row.appendChild(cell); this._contentTable.appendChild(row); }, _appendLinkRow: function(title, scriptName, scriptLine) { var link = WebInspector.linkifyResourceAsNode(scriptName, "scripts", scriptLine, "timeline-details"); this._appendElementRow(title, link); }, _appendStackTrace: function(title, stackTrace) { this._appendTextRow("", ""); var framesTable = document.createElement("table"); for (var i = 0; i < stackTrace.length; ++i) { var stackFrame = stackTrace[i]; var row = document.createElement("tr"); row.className = "timeline-details"; row.appendChild(this._createCell(stackFrame.functionName ? stackFrame.functionName : WebInspector.UIString("(anonymous function)"), "timeline-function-name")); row.appendChild(this._createCell(" @ ")); var linkCell = document.createElement("td"); linkCell.appendChild(WebInspector.linkifyResourceAsNode(stackFrame.scriptName, "scripts", stackFrame.lineNumber, "timeline-details")); row.appendChild(linkCell); framesTable.appendChild(row); } this._appendElementRow(title, framesTable, "timeline-stacktrace-title"); } } WebInspector.TimelineExpandableElement = function(container) { this._element = document.createElement("div"); this._element.className = "timeline-expandable"; var leftBorder = document.createElement("div"); leftBorder.className = "timeline-expandable-left"; this._element.appendChild(leftBorder); container.appendChild(this._element); } WebInspector.TimelineExpandableElement.prototype = { _update: function(record, index, barPosition) { const rowHeight = WebInspector.TimelinePanel.rowHeight; if (record._visibleChildrenCount || record._invisibleChildrenCount) { this._element.style.top = index * rowHeight + "px"; this._element.style.left = barPosition.left + "px"; this._element.style.width = Math.max(12, barPosition.width + 25) + "px"; if (!record.collapsed) { this._element.style.height = (record._visibleChildrenCount + 1) * rowHeight + "px"; this._element.addStyleClass("timeline-expandable-expanded"); this._element.removeStyleClass("timeline-expandable-collapsed"); } else { this._element.style.height = rowHeight + "px"; this._element.addStyleClass("timeline-expandable-collapsed"); this._element.removeStyleClass("timeline-expandable-expanded"); } this._element.removeStyleClass("hidden"); } else this._element.addStyleClass("hidden"); }, _dispose: function() { this._element.parentElement.removeChild(this._element); } }