summaryrefslogtreecommitdiffstats
path: root/chromium/third_party/WebKit/Source/devtools/front_end/timeline/TimelineView.js
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/WebKit/Source/devtools/front_end/timeline/TimelineView.js')
-rw-r--r--chromium/third_party/WebKit/Source/devtools/front_end/timeline/TimelineView.js1304
1 files changed, 1304 insertions, 0 deletions
diff --git a/chromium/third_party/WebKit/Source/devtools/front_end/timeline/TimelineView.js b/chromium/third_party/WebKit/Source/devtools/front_end/timeline/TimelineView.js
new file mode 100644
index 00000000000..5b5fc6333eb
--- /dev/null
+++ b/chromium/third_party/WebKit/Source/devtools/front_end/timeline/TimelineView.js
@@ -0,0 +1,1304 @@
+/*
+ * Copyright (C) 2013 Google Inc. All rights reserved.
+ * Copyright (C) 2012 Intel 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.
+ */
+
+/**
+ * @constructor
+ * @extends {WebInspector.HBox}
+ * @implements {WebInspector.TimelineModeView}
+ * @param {!WebInspector.TimelineModeViewDelegate} delegate
+ * @param {!WebInspector.TimelineModel} model
+ * @param {!WebInspector.TimelineUIUtils} uiUtils
+ */
+WebInspector.TimelineView = function(delegate, model, uiUtils)
+{
+ WebInspector.HBox.call(this);
+ this.element.classList.add("timeline-view");
+
+ this._delegate = delegate;
+ this._model = model;
+ this._uiUtils = uiUtils;
+ this._presentationModel = new WebInspector.TimelinePresentationModel(model, uiUtils);
+ this._calculator = new WebInspector.TimelineCalculator(model);
+ this._linkifier = new WebInspector.Linkifier();
+ this._frameStripByFrame = new Map();
+
+ this._boundariesAreValid = true;
+ this._scrollTop = 0;
+
+ this._recordsView = this._createRecordsView();
+ this._recordsView.addEventListener(WebInspector.SplitView.Events.SidebarSizeChanged, this._sidebarResized, this);
+ this._recordsView.show(this.element);
+ this._headerElement = this.element.createChild("div", "fill");
+ this._headerElement.id = "timeline-graph-records-header";
+
+ // Create gpu tasks containers.
+ this._cpuBarsElement = this._headerElement.createChild("div", "timeline-utilization-strip");
+ if (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
+ this._gpuBarsElement = this._headerElement.createChild("div", "timeline-utilization-strip gpu");
+
+ this._popoverHelper = new WebInspector.PopoverHelper(this.element, this._getPopoverAnchor.bind(this), this._showPopover.bind(this));
+
+ this.element.addEventListener("mousemove", this._mouseMove.bind(this), false);
+ this.element.addEventListener("mouseout", this._mouseOut.bind(this), false);
+ this.element.addEventListener("keydown", this._keyDown.bind(this), false);
+
+ this._expandOffset = 15;
+}
+
+WebInspector.TimelineView.prototype = {
+ /**
+ * @param {?WebInspector.TimelineFrameModelBase} frameModel
+ */
+ setFrameModel: function(frameModel)
+ {
+ this._frameModel = frameModel;
+ },
+
+ /**
+ * @return {!WebInspector.SplitView}
+ */
+ _createRecordsView: function()
+ {
+ var recordsView = new WebInspector.SplitView(true, false, "timelinePanelRecorsSplitViewState");
+ this._containerElement = recordsView.element;
+ this._containerElement.tabIndex = 0;
+ this._containerElement.id = "timeline-container";
+ this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
+
+ // Create records list in the records sidebar.
+ recordsView.sidebarElement().createChild("div", "timeline-records-title").textContent = WebInspector.UIString("RECORDS");
+ this._sidebarListElement = recordsView.sidebarElement().createChild("div", "timeline-records-list");
+
+ // Create grid in the records main area.
+ this._gridContainer = new WebInspector.VBoxWithResizeCallback(this._onViewportResize.bind(this));
+ this._gridContainer.element.id = "resources-container-content";
+ this._gridContainer.show(recordsView.mainElement());
+ this._timelineGrid = new WebInspector.TimelineGrid();
+ this._gridContainer.element.appendChild(this._timelineGrid.element);
+
+ this._itemsGraphsElement = this._gridContainer.element.createChild("div");
+ this._itemsGraphsElement.id = "timeline-graphs";
+
+ // Create gap elements
+ this._topGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap");
+ this._graphRowsElement = this._itemsGraphsElement.createChild("div");
+ this._bottomGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap");
+ this._expandElements = this._itemsGraphsElement.createChild("div");
+ this._expandElements.id = "orphan-expand-elements";
+
+ return recordsView;
+ },
+
+ _rootRecord: function()
+ {
+ return this._presentationModel.rootRecord();
+ },
+
+ _updateEventDividers: function()
+ {
+ this._timelineGrid.removeEventDividers();
+ var clientWidth = this._graphRowsElementWidth;
+ var dividers = [];
+ var eventDividerRecords = this._model.eventDividerRecords();
+
+ for (var i = 0; i < eventDividerRecords.length; ++i) {
+ var record = eventDividerRecords[i];
+ var position = this._calculator.computePosition(record.startTime());
+ var dividerPosition = Math.round(position);
+ if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition])
+ continue;
+ var title = this._uiUtils.titleForRecord(record);
+ var divider = this._uiUtils.createEventDivider(record.type(), title);
+ divider.style.left = dividerPosition + "px";
+ dividers[dividerPosition] = divider;
+ }
+ this._timelineGrid.addEventDividers(dividers);
+ },
+
+ _updateFrameBars: function(frames)
+ {
+ var clientWidth = this._graphRowsElementWidth;
+ if (this._frameContainer) {
+ this._frameContainer.removeChildren();
+ } else {
+ const frameContainerBorderWidth = 1;
+ this._frameContainer = document.createElement("div");
+ this._frameContainer.classList.add("fill");
+ this._frameContainer.classList.add("timeline-frame-container");
+ this._frameContainer.style.height = WebInspector.TimelinePanel.rowHeight + frameContainerBorderWidth + "px";
+ this._frameContainer.addEventListener("dblclick", this._onFrameDoubleClicked.bind(this), false);
+ this._frameContainer.addEventListener("click", this._onFrameClicked.bind(this), false);
+ }
+ this._frameStripByFrame.clear();
+
+ var dividers = [];
+
+ for (var i = 0; i < frames.length; ++i) {
+ var frame = frames[i];
+ var frameStart = this._calculator.computePosition(frame.startTime);
+ var frameEnd = this._calculator.computePosition(frame.endTime);
+
+ var frameStrip = document.createElement("div");
+ frameStrip.className = "timeline-frame-strip";
+ var actualStart = Math.max(frameStart, 0);
+ var width = frameEnd - actualStart;
+ frameStrip.style.left = actualStart + "px";
+ frameStrip.style.width = width + "px";
+ frameStrip._frame = frame;
+ this._frameStripByFrame.put(frame, frameStrip);
+
+ const minWidthForFrameInfo = 60;
+ if (width > minWidthForFrameInfo)
+ frameStrip.textContent = Number.millisToString(frame.endTime - frame.startTime, true);
+
+ this._frameContainer.appendChild(frameStrip);
+
+ if (actualStart > 0) {
+ var frameMarker = this._uiUtils.createBeginFrameDivider();
+ frameMarker.style.left = frameStart + "px";
+ dividers.push(frameMarker);
+ }
+ }
+ this._timelineGrid.addEventDividers(dividers);
+ this._headerElement.appendChild(this._frameContainer);
+ },
+
+ _onFrameDoubleClicked: function(event)
+ {
+ var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip");
+ if (!frameBar)
+ return;
+ this._delegate.requestWindowTimes(frameBar._frame.startTime, frameBar._frame.endTime);
+ },
+
+ _onFrameClicked: function(event)
+ {
+ var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip");
+ if (!frameBar)
+ return;
+ this._delegate.select(WebInspector.TimelineSelection.fromFrame(frameBar._frame));
+ },
+
+ /**
+ * @param {!WebInspector.TimelineModel.Record} record
+ */
+ addRecord: function(record)
+ {
+ this._presentationModel.addRecord(record);
+ this._invalidateAndScheduleRefresh(false, false);
+ },
+
+ /**
+ * @param {number} width
+ */
+ setSidebarSize: function(width)
+ {
+ this._recordsView.setSidebarSize(width);
+ },
+
+ /**
+ * @param {!WebInspector.Event} event
+ */
+ _sidebarResized: function(event)
+ {
+ this.dispatchEventToListeners(WebInspector.SplitView.Events.SidebarSizeChanged, event.data);
+ },
+
+ _onViewportResize: function()
+ {
+ this._resize(this._recordsView.sidebarSize());
+ },
+
+ /**
+ * @param {number} sidebarWidth
+ */
+ _resize: function(sidebarWidth)
+ {
+ this._closeRecordDetails();
+ this._graphRowsElementWidth = this._graphRowsElement.offsetWidth;
+ this._headerElement.style.left = sidebarWidth + "px";
+ this._headerElement.style.width = this._itemsGraphsElement.offsetWidth + "px";
+ this._scheduleRefresh(false, true);
+ },
+
+ _resetView: function()
+ {
+ this._windowStartTime = 0;
+ this._windowEndTime = 0;
+ this._boundariesAreValid = false;
+ this._adjustScrollPosition(0);
+ this._linkifier.reset();
+ this._closeRecordDetails();
+ this._automaticallySizeWindow = true;
+ this._presentationModel.reset();
+ },
+
+
+ /**
+ * @return {!WebInspector.View}
+ */
+ view: function()
+ {
+ return this;
+ },
+
+ dispose: function()
+ {
+ },
+
+ reset: function()
+ {
+ this._resetView();
+ this._invalidateAndScheduleRefresh(true, true);
+ },
+
+ /**
+ * @return {!Array.<!Element>}
+ */
+ elementsToRestoreScrollPositionsFor: function()
+ {
+ return [this._containerElement];
+ },
+
+ /**
+ * @param {?RegExp} textFilter
+ */
+ refreshRecords: function(textFilter)
+ {
+ this._presentationModel.reset();
+ var records = this._model.records();
+ for (var i = 0; i < records.length; ++i)
+ this.addRecord(records[i]);
+ this._automaticallySizeWindow = false;
+ this._presentationModel.setTextFilter(textFilter);
+ this._invalidateAndScheduleRefresh(false, true);
+ },
+
+ willHide: function()
+ {
+ this._closeRecordDetails();
+ WebInspector.View.prototype.willHide.call(this);
+ },
+
+ _onScroll: function(event)
+ {
+ this._closeRecordDetails();
+ this._scrollTop = this._containerElement.scrollTop;
+ var dividersTop = Math.max(0, this._scrollTop);
+ this._timelineGrid.setScrollAndDividerTop(this._scrollTop, dividersTop);
+ this._scheduleRefresh(true, true);
+ },
+
+ /**
+ * @param {boolean} preserveBoundaries
+ * @param {boolean} userGesture
+ */
+ _invalidateAndScheduleRefresh: function(preserveBoundaries, userGesture)
+ {
+ this._presentationModel.invalidateFilteredRecords();
+ this._scheduleRefresh(preserveBoundaries, userGesture);
+ },
+
+ _clearSelection: function()
+ {
+ this._delegate.select(null);
+ },
+
+ /**
+ * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord
+ */
+ _selectRecord: function(presentationRecord)
+ {
+ if (presentationRecord.coalesced()) {
+ // Presentation record does not have model record to highlight.
+ this._innerSetSelectedRecord(presentationRecord);
+ var aggregatedStats = {};
+ var presentationChildren = presentationRecord.presentationChildren();
+ for (var i = 0; i < presentationChildren.length; ++i)
+ WebInspector.TimelineUIUtils.aggregateTimeByCategory(aggregatedStats, presentationChildren[i].record().aggregatedStats());
+ var idle = presentationRecord.record().endTime() - presentationRecord.record().startTime();
+ for (var category in aggregatedStats)
+ idle -= aggregatedStats[category];
+ aggregatedStats["idle"] = idle;
+ var pieChart = WebInspector.TimelineUIUtils.generatePieChart(aggregatedStats);
+ this._delegate.showInDetails(WebInspector.TimelineUIUtils.recordStyle(presentationRecord.record()).title, pieChart);
+ return;
+ }
+ this._delegate.select(WebInspector.TimelineSelection.fromRecord(presentationRecord.record()));
+ },
+
+ /**
+ * @param {?WebInspector.TimelineSelection} selection
+ */
+ setSelection: function(selection)
+ {
+ if (!selection) {
+ this._innerSetSelectedRecord(null);
+ this._setSelectedFrame(null);
+ return;
+ }
+ if (selection.type() === WebInspector.TimelineSelection.Type.Record) {
+ var record = /** @type {!WebInspector.TimelineModel.Record} */ (selection.object());
+ this._innerSetSelectedRecord(this._presentationModel.toPresentationRecord(record));
+ this._setSelectedFrame(null);
+ } else if (selection.type() === WebInspector.TimelineSelection.Type.Frame) {
+ var frame = /** @type {!WebInspector.TimelineFrame} */ (selection.object());
+ this._innerSetSelectedRecord(null);
+ this._setSelectedFrame(frame);
+ }
+ },
+
+ /**
+ * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord
+ */
+ _innerSetSelectedRecord: function(presentationRecord)
+ {
+ if (presentationRecord === this._lastSelectedRecord)
+ return;
+
+ // Remove selection rendering.p
+ if (this._lastSelectedRecord) {
+ if (this._lastSelectedRecord.listRow())
+ this._lastSelectedRecord.listRow().renderAsSelected(false);
+ if (this._lastSelectedRecord.graphRow())
+ this._lastSelectedRecord.graphRow().renderAsSelected(false);
+ }
+
+ this._lastSelectedRecord = presentationRecord;
+ if (!presentationRecord)
+ return;
+
+ this._innerRevealRecord(presentationRecord);
+ if (presentationRecord.listRow())
+ presentationRecord.listRow().renderAsSelected(true);
+ if (presentationRecord.graphRow())
+ presentationRecord.graphRow().renderAsSelected(true);
+ },
+
+ /**
+ * @param {?WebInspector.TimelineFrame} frame
+ */
+ _setSelectedFrame: function(frame)
+ {
+ if (this._lastSelectedFrame === frame)
+ return;
+ var oldStripElement = this._lastSelectedFrame && this._frameStripByFrame.get(this._lastSelectedFrame);
+ if (oldStripElement)
+ oldStripElement.classList.remove("selected");
+ var newStripElement = frame && this._frameStripByFrame.get(frame);
+ if (newStripElement)
+ newStripElement.classList.add("selected");
+ this._lastSelectedFrame = frame;
+ },
+
+ /**
+ * @param {number} startTime
+ * @param {number} endTime
+ */
+ setWindowTimes: function(startTime, endTime)
+ {
+ this._windowStartTime = startTime;
+ this._windowEndTime = endTime;
+ this._presentationModel.setWindowTimes(startTime, endTime);
+ this._automaticallySizeWindow = false;
+ this._invalidateAndScheduleRefresh(false, true);
+ this._clearSelection();
+ },
+
+ /**
+ * @param {boolean} preserveBoundaries
+ * @param {boolean} userGesture
+ */
+ _scheduleRefresh: function(preserveBoundaries, userGesture)
+ {
+ this._closeRecordDetails();
+ this._boundariesAreValid &= preserveBoundaries;
+
+ if (!this.isShowing())
+ return;
+
+ if (preserveBoundaries || userGesture)
+ this._refresh();
+ else {
+ if (!this._refreshTimeout)
+ this._refreshTimeout = setTimeout(this._refresh.bind(this), 300);
+ }
+ },
+
+ _refresh: function()
+ {
+ if (this._refreshTimeout) {
+ clearTimeout(this._refreshTimeout);
+ delete this._refreshTimeout;
+ }
+ var windowStartTime = this._windowStartTime || this._model.minimumRecordTime();
+ var windowEndTime = this._windowEndTime || this._model.maximumRecordTime();
+ this._timelinePaddingLeft = this._expandOffset;
+ this._calculator.setWindow(windowStartTime, windowEndTime);
+ this._calculator.setDisplayWindow(this._timelinePaddingLeft, this._graphRowsElementWidth);
+
+ this._refreshRecords();
+ if (!this._boundariesAreValid) {
+ this._updateEventDividers();
+ if (this._frameContainer)
+ this._frameContainer.remove();
+ if (this._frameModel) {
+ var frames = this._frameModel.filteredFrames(windowStartTime, windowEndTime);
+ const maxFramesForFrameBars = 30;
+ if (frames.length && frames.length < maxFramesForFrameBars) {
+ this._timelineGrid.removeDividers();
+ this._updateFrameBars(frames);
+ } else {
+ this._timelineGrid.updateDividers(this._calculator);
+ }
+ } else
+ this._timelineGrid.updateDividers(this._calculator);
+ this._refreshAllUtilizationBars();
+ }
+ this._boundariesAreValid = true;
+ },
+
+ /**
+ * @param {!WebInspector.TimelinePresentationModel.Record} recordToReveal
+ */
+ _innerRevealRecord: function(recordToReveal)
+ {
+ var needRefresh = false;
+ // Expand all ancestors.
+ for (var parent = recordToReveal.presentationParent(); parent !== this._rootRecord(); parent = parent.presentationParent()) {
+ if (!parent.collapsed())
+ continue;
+ this._presentationModel.invalidateFilteredRecords();
+ parent.setCollapsed(false);
+ needRefresh = true;
+ }
+ var recordsInWindow = this._presentationModel.filteredRecords();
+ var index = recordsInWindow.indexOf(recordToReveal);
+
+ var itemOffset = index * WebInspector.TimelinePanel.rowHeight;
+ var visibleTop = this._scrollTop - WebInspector.TimelinePanel.headerHeight;
+ var visibleBottom = visibleTop + this._containerElementHeight - WebInspector.TimelinePanel.rowHeight;
+ if (itemOffset < visibleTop)
+ this._containerElement.scrollTop = itemOffset;
+ else if (itemOffset > visibleBottom)
+ this._containerElement.scrollTop = itemOffset - this._containerElementHeight + WebInspector.TimelinePanel.headerHeight + WebInspector.TimelinePanel.rowHeight;
+ else if (needRefresh)
+ this._refreshRecords();
+ },
+
+ _refreshRecords: function()
+ {
+ this._containerElementHeight = this._containerElement.clientHeight;
+ var recordsInWindow = this._presentationModel.filteredRecords();
+
+ // Calculate the visible area.
+ var visibleTop = this._scrollTop;
+ var visibleBottom = visibleTop + this._containerElementHeight;
+
+ var rowHeight = WebInspector.TimelinePanel.rowHeight;
+ var headerHeight = WebInspector.TimelinePanel.headerHeight;
+
+ // 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 - headerHeight) / rowHeight), recordsInWindow.length - 1));
+ var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
+ var lastVisibleLine = Math.max(0, Math.floor((visibleBottom - headerHeight) / rowHeight));
+ if (this._automaticallySizeWindow && recordsInWindow.length > lastVisibleLine) {
+ this._automaticallySizeWindow = false;
+ this._clearSelection();
+ // If we're at the top, always use real timeline start as a left window bound so that expansion arrow padding logic works.
+ var windowStartTime = startIndex ? recordsInWindow[startIndex].startTime() : this._model.minimumRecordTime();
+ var windowEndTime = recordsInWindow[Math.max(0, lastVisibleLine - 1)].endTime();
+ this._delegate.requestWindowTimes(windowStartTime, windowEndTime);
+ recordsInWindow = this._presentationModel.filteredRecords();
+ endIndex = Math.min(recordsInWindow.length, lastVisibleLine);
+ }
+
+ // Resize gaps first.
+ this._topGapElement.style.height = (startIndex * rowHeight) + "px";
+ this._recordsView.sidebarElement().firstElementChild.style.flexBasis = (startIndex * rowHeight + headerHeight) + "px";
+ this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
+ var rowsHeight = headerHeight + recordsInWindow.length * rowHeight;
+ var totalHeight = Math.max(this._containerElementHeight, rowsHeight);
+
+ this._recordsView.mainElement().style.height = totalHeight + "px";
+ this._recordsView.sidebarElement().style.height = totalHeight + "px";
+ this._recordsView.resizerElement().style.height = totalHeight + "px";
+
+ // Update visible rows.
+ var listRowElement = this._sidebarListElement.firstChild;
+ var width = this._graphRowsElementWidth;
+ this._itemsGraphsElement.removeChild(this._graphRowsElement);
+ var graphRowElement = this._graphRowsElement.firstChild;
+ var scheduleRefreshCallback = this._invalidateAndScheduleRefresh.bind(this, true, true);
+ var selectRecordCallback = this._selectRecord.bind(this);
+ this._itemsGraphsElement.removeChild(this._expandElements);
+ this._expandElements.removeChildren();
+
+ for (var i = 0; i < endIndex; ++i) {
+ var record = recordsInWindow[i];
+
+ if (i < startIndex) {
+ var lastChildIndex = i + record.visibleChildrenCount();
+ if (lastChildIndex >= startIndex && lastChildIndex < endIndex) {
+ var expandElement = new WebInspector.TimelineExpandableElement(this._expandElements);
+ var positions = this._calculator.computeBarGraphWindowPosition(record);
+ expandElement._update(record, i, positions.left - this._expandOffset, positions.width);
+ }
+ } else {
+ if (!listRowElement) {
+ listRowElement = new WebInspector.TimelineRecordListRow(this._linkifier, selectRecordCallback, scheduleRefreshCallback).element;
+ this._sidebarListElement.appendChild(listRowElement);
+ }
+ if (!graphRowElement) {
+ graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, selectRecordCallback, scheduleRefreshCallback).element;
+ this._graphRowsElement.appendChild(graphRowElement);
+ }
+
+ listRowElement.row.update(record, visibleTop, this._model.loadedFromFile(), this._uiUtils);
+ graphRowElement.row.update(record, this._calculator, this._expandOffset, i);
+ if (this._lastSelectedRecord === record) {
+ listRowElement.row.renderAsSelected(true);
+ graphRowElement.row.renderAsSelected(true);
+ }
+
+ 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._adjustScrollPosition(recordsInWindow.length * rowHeight + headerHeight);
+
+ return recordsInWindow.length;
+ },
+
+ _refreshAllUtilizationBars: function()
+ {
+ this._refreshUtilizationBars(WebInspector.UIString("CPU"), this._model.mainThreadTasks(), this._cpuBarsElement);
+ if (WebInspector.experimentsSettings.gpuTimeline.isEnabled())
+ this._refreshUtilizationBars(WebInspector.UIString("GPU"), this._model.gpuThreadTasks(), this._gpuBarsElement);
+ },
+
+ /**
+ * @param {string} name
+ * @param {!Array.<!WebInspector.TimelineModel.Record>} tasks
+ * @param {?Element} container
+ */
+ _refreshUtilizationBars: function(name, tasks, container)
+ {
+ if (!container)
+ return;
+
+ const barOffset = 3;
+ const minGap = 3;
+
+ var minWidth = WebInspector.TimelineCalculator._minWidth;
+ var widthAdjustment = minWidth / 2;
+
+ var width = this._graphRowsElementWidth;
+ var boundarySpan = this._windowEndTime - this._windowStartTime;
+ var scale = boundarySpan / (width - minWidth - this._timelinePaddingLeft);
+ var startTime = (this._windowStartTime - this._timelinePaddingLeft * scale);
+ var endTime = startTime + width * scale;
+
+ /**
+ * @param {number} value
+ * @param {!WebInspector.TimelineModel.Record} task
+ * @return {number}
+ */
+ function compareEndTime(value, task)
+ {
+ return value < task.endTime() ? -1 : 1;
+ }
+
+ var taskIndex = insertionIndexForObjectInListSortedByFunction(startTime, tasks, compareEndTime);
+
+ var foreignStyle = "gpu-task-foreign";
+ var element = /** @type {?Element} */ (container.firstChild);
+ var lastElement;
+ var lastLeft;
+ var lastRight;
+
+ for (; taskIndex < tasks.length; ++taskIndex) {
+ var task = tasks[taskIndex];
+ if (task.startTime() > endTime)
+ break;
+
+ var left = Math.max(0, this._calculator.computePosition(task.startTime()) + barOffset - widthAdjustment);
+ var right = Math.min(width, this._calculator.computePosition(task.endTime() || 0) + barOffset + widthAdjustment);
+
+ if (lastElement) {
+ var gap = Math.floor(left) - Math.ceil(lastRight);
+ if (gap < minGap) {
+ if (!task.data["foreign"])
+ lastElement.classList.remove(foreignStyle);
+ lastRight = right;
+ lastElement._tasksInfo.lastTaskIndex = taskIndex;
+ continue;
+ }
+ lastElement.style.width = (lastRight - lastLeft) + "px";
+ }
+
+ if (!element)
+ element = container.createChild("div", "timeline-graph-bar");
+ element.style.left = left + "px";
+ element._tasksInfo = {name: name, tasks: tasks, firstTaskIndex: taskIndex, lastTaskIndex: taskIndex};
+ if (task.data["foreign"])
+ element.classList.add(foreignStyle);
+ lastLeft = left;
+ lastRight = right;
+ lastElement = element;
+ element = element.nextSibling;
+ }
+
+ if (lastElement)
+ lastElement.style.width = (lastRight - lastLeft) + "px";
+
+ while (element) {
+ var nextElement = element.nextSibling;
+ element._tasksInfo = null;
+ container.removeChild(element);
+ element = nextElement;
+ }
+ },
+
+ _adjustScrollPosition: function(totalHeight)
+ {
+ // Prevent the container from being scrolled off the end.
+ if ((this._scrollTop + this._containerElementHeight) > totalHeight + 1)
+ this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
+ },
+
+ _getPopoverAnchor: function(element)
+ {
+ var anchor = element.enclosingNodeOrSelfWithClass("timeline-graph-bar");
+ if (anchor && anchor._tasksInfo)
+ return anchor;
+ return null;
+ },
+
+ _mouseOut: function()
+ {
+ this._hideQuadHighlight();
+ },
+
+ /**
+ * @param {?Event} e
+ */
+ _mouseMove: function(e)
+ {
+ var rowElement = e.target.enclosingNodeOrSelfWithClass("timeline-tree-item");
+ if (!this._highlightQuad(rowElement))
+ this._hideQuadHighlight();
+
+ var taskBarElement = e.target.enclosingNodeOrSelfWithClass("timeline-graph-bar");
+ if (taskBarElement && taskBarElement._tasksInfo) {
+ var offset = taskBarElement.offsetLeft;
+ this._timelineGrid.showCurtains(offset >= 0 ? offset : 0, taskBarElement.offsetWidth);
+ } else
+ this._timelineGrid.hideCurtains();
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _keyDown: function(event)
+ {
+ if (!this._lastSelectedRecord || event.shiftKey || event.metaKey || event.ctrlKey)
+ return;
+
+ var record = this._lastSelectedRecord;
+ var recordsInWindow = this._presentationModel.filteredRecords();
+ var index = recordsInWindow.indexOf(record);
+ var recordsInPage = Math.floor(this._containerElementHeight / WebInspector.TimelinePanel.rowHeight);
+ var rowHeight = WebInspector.TimelinePanel.rowHeight;
+
+ if (index === -1)
+ index = 0;
+
+ switch (event.keyIdentifier) {
+ case "Left":
+ if (record.presentationParent()) {
+ if ((!record.expandable() || record.collapsed()) && record.presentationParent() !== this._presentationModel.rootRecord()) {
+ this._selectRecord(record.presentationParent());
+ } else {
+ record.setCollapsed(true);
+ this._invalidateAndScheduleRefresh(true, true);
+ }
+ }
+ event.consume(true);
+ break;
+ case "Up":
+ if (--index < 0)
+ break;
+ this._selectRecord(recordsInWindow[index]);
+ event.consume(true);
+ break;
+ case "Right":
+ if (record.expandable() && record.collapsed()) {
+ record.setCollapsed(false);
+ this._invalidateAndScheduleRefresh(true, true);
+ } else {
+ if (++index >= recordsInWindow.length)
+ break;
+ this._selectRecord(recordsInWindow[index]);
+ }
+ event.consume(true);
+ break;
+ case "Down":
+ if (++index >= recordsInWindow.length)
+ break;
+ this._selectRecord(recordsInWindow[index]);
+ event.consume(true);
+ break;
+ case "PageUp":
+ index = Math.max(0, index - recordsInPage);
+ this._scrollTop = Math.max(0, this._scrollTop - recordsInPage * rowHeight);
+ this._containerElement.scrollTop = this._scrollTop;
+ this._selectRecord(recordsInWindow[index]);
+ event.consume(true);
+ break;
+ case "PageDown":
+ index = Math.min(recordsInWindow.length - 1, index + recordsInPage);
+ this._scrollTop = Math.min(this._containerElement.scrollHeight - this._containerElementHeight, this._scrollTop + recordsInPage * rowHeight);
+ this._containerElement.scrollTop = this._scrollTop;
+ this._selectRecord(recordsInWindow[index]);
+ event.consume(true);
+ break;
+ case "Home":
+ index = 0;
+ this._selectRecord(recordsInWindow[index]);
+ event.consume(true);
+ break;
+ case "End":
+ index = recordsInWindow.length - 1;
+ this._selectRecord(recordsInWindow[index]);
+ event.consume(true);
+ break;
+ }
+ },
+
+ /**
+ * @param {?Element} rowElement
+ * @return {boolean}
+ */
+ _highlightQuad: function(rowElement)
+ {
+ if (!rowElement || !rowElement.row)
+ return false;
+ var presentationRecord = rowElement.row._record;
+ if (presentationRecord.coalesced())
+ return false;
+ var record = presentationRecord.record();
+ if (this._highlightedQuadRecord === record)
+ return true;
+ this._highlightedQuadRecord = record;
+
+ var quad = this._uiUtils.highlightQuadForRecord(record);
+ if (!quad)
+ return false;
+ record.target().domAgent().highlightQuad(quad, WebInspector.Color.PageHighlight.Content.toProtocolRGBA(), WebInspector.Color.PageHighlight.ContentOutline.toProtocolRGBA());
+ return true;
+ },
+
+ _hideQuadHighlight: function()
+ {
+ if (this._highlightedQuadRecord) {
+ this._highlightedQuadRecord.target().domAgent().hideHighlight();
+ delete this._highlightedQuadRecord;
+ }
+ },
+
+ /**
+ * @param {!Element} anchor
+ * @param {!WebInspector.Popover} popover
+ */
+ _showPopover: function(anchor, popover)
+ {
+ if (!anchor._tasksInfo)
+ return;
+ popover.show(WebInspector.TimelineUIUtils.generateMainThreadBarPopupContent(this._model, anchor._tasksInfo), anchor, null, null, WebInspector.Popover.Orientation.Bottom);
+ },
+
+ _closeRecordDetails: function()
+ {
+ this._popoverHelper.hidePopover();
+ },
+
+ /**
+ * @param {?WebInspector.TimelineModel.Record} record
+ * @param {string=} regex
+ * @param {boolean=} selectRecord
+ */
+ highlightSearchResult: function(record, regex, selectRecord)
+ {
+ if (this._highlightDomChanges)
+ WebInspector.revertDomChanges(this._highlightDomChanges);
+ this._highlightDomChanges = [];
+
+ var presentationRecord = this._presentationModel.toPresentationRecord(record);
+ if (!presentationRecord)
+ return;
+
+ if (selectRecord)
+ this._selectRecord(presentationRecord);
+
+ for (var element = this._sidebarListElement.firstChild; element; element = element.nextSibling) {
+ if (element.row._record === presentationRecord) {
+ element.row.highlight(regex, this._highlightDomChanges);
+ break;
+ }
+ }
+ },
+
+ __proto__: WebInspector.HBox.prototype
+}
+
+/**
+ * @constructor
+ * @param {!WebInspector.TimelineModel} model
+ * @implements {WebInspector.TimelineGrid.Calculator}
+ */
+WebInspector.TimelineCalculator = function(model)
+{
+ this._model = model;
+}
+
+WebInspector.TimelineCalculator._minWidth = 5;
+
+WebInspector.TimelineCalculator.prototype = {
+ /**
+ * @return {number}
+ */
+ paddingLeft: function()
+ {
+ return this._paddingLeft;
+ },
+
+ /**
+ * @param {number} time
+ * @return {number}
+ */
+ computePosition: function(time)
+ {
+ return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this._paddingLeft;
+ },
+
+ /**
+ * @param {!WebInspector.TimelinePresentationModel.Record} record
+ * @return {!{start: number, end: number, cpuWidth: number}}
+ */
+ computeBarGraphPercentages: function(record)
+ {
+ var start = (record.startTime() - this._minimumBoundary) / this.boundarySpan() * 100;
+ var end = (record.startTime() + record.selfTime() - this._minimumBoundary) / this.boundarySpan() * 100;
+ var cpuWidth = (record.endTime() - record.startTime()) / this.boundarySpan() * 100;
+ return {start: start, end: end, cpuWidth: cpuWidth};
+ },
+
+ /**
+ * @param {!WebInspector.TimelinePresentationModel.Record} record
+ * @return {!{left: number, width: number, cpuWidth: number}}
+ */
+ computeBarGraphWindowPosition: function(record)
+ {
+ var percentages = this.computeBarGraphPercentages(record);
+ var widthAdjustment = 0;
+
+ var left = this.computePosition(record.startTime());
+ var width = (percentages.end - percentages.start) / 100 * this._workingArea;
+ if (width < WebInspector.TimelineCalculator._minWidth) {
+ widthAdjustment = WebInspector.TimelineCalculator._minWidth - width;
+ width = WebInspector.TimelineCalculator._minWidth;
+ }
+ var cpuWidth = percentages.cpuWidth / 100 * this._workingArea + widthAdjustment;
+ return {left: left, width: width, cpuWidth: cpuWidth};
+ },
+
+ setWindow: function(minimumBoundary, maximumBoundary)
+ {
+ this._minimumBoundary = minimumBoundary;
+ this._maximumBoundary = maximumBoundary;
+ },
+
+ /**
+ * @param {number} paddingLeft
+ * @param {number} clientWidth
+ */
+ setDisplayWindow: function(paddingLeft, clientWidth)
+ {
+ this._workingArea = clientWidth - WebInspector.TimelineCalculator._minWidth - paddingLeft;
+ this._paddingLeft = paddingLeft;
+ },
+
+ /**
+ * @param {number} value
+ * @param {number=} precision
+ * @return {string}
+ */
+ formatTime: function(value, precision)
+ {
+ return Number.preciseMillisToString(value - this.zeroTime(), precision);
+ },
+
+ /**
+ * @return {number}
+ */
+ maximumBoundary: function()
+ {
+ return this._maximumBoundary;
+ },
+
+ /**
+ * @return {number}
+ */
+ minimumBoundary: function()
+ {
+ return this._minimumBoundary;
+ },
+
+ /**
+ * @return {number}
+ */
+ zeroTime: function()
+ {
+ return this._model.minimumRecordTime();
+ },
+
+ /**
+ * @return {number}
+ */
+ boundarySpan: function()
+ {
+ return this._maximumBoundary - this._minimumBoundary;
+ }
+}
+
+/**
+ * @constructor
+ * @param {!WebInspector.Linkifier} linkifier
+ * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
+ * @param {function()} scheduleRefresh
+ */
+WebInspector.TimelineRecordListRow = function(linkifier, selectRecord, scheduleRefresh)
+{
+ this.element = document.createElement("div");
+ this.element.row = this;
+ this.element.style.cursor = "pointer";
+ this.element.addEventListener("click", this._onClick.bind(this), false);
+ this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
+ this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false);
+ this._linkifier = linkifier;
+
+ // Warning is float right block, it goes first.
+ this._warningElement = this.element.createChild("div", "timeline-tree-item-warning hidden");
+
+ this._expandArrowElement = this.element.createChild("div", "timeline-tree-item-expand-arrow");
+ this._expandArrowElement.addEventListener("click", this._onExpandClick.bind(this), false);
+ var iconElement = this.element.createChild("span", "timeline-tree-icon");
+ this._typeElement = this.element.createChild("span", "type");
+
+ this._dataElement = this.element.createChild("span", "data dimmed");
+ this._scheduleRefresh = scheduleRefresh;
+ this._selectRecord = selectRecord;
+}
+
+WebInspector.TimelineRecordListRow.prototype = {
+ /**
+ * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
+ * @param {number} offset
+ * @param {boolean} loadedFromFile
+ * @param {!WebInspector.TimelineUIUtils} uiUtils
+ */
+ update: function(presentationRecord, offset, loadedFromFile, uiUtils)
+ {
+ this._record = presentationRecord;
+ var record = presentationRecord.record();
+ this._offset = offset;
+
+ this.element.className = "timeline-tree-item timeline-category-" + record.category().name;
+ var paddingLeft = 5;
+ var step = -3;
+ for (var currentRecord = presentationRecord.presentationParent() ? presentationRecord.presentationParent().presentationParent() : null; currentRecord; currentRecord = currentRecord.presentationParent())
+ paddingLeft += 12 / (Math.max(1, step++));
+ this.element.style.paddingLeft = paddingLeft + "px";
+ if (record.thread())
+ this.element.classList.add("background");
+
+ this._typeElement.textContent = uiUtils.titleForRecord(record);
+
+ if (this._dataElement.firstChild)
+ this._dataElement.removeChildren();
+
+ this._warningElement.classList.toggle("hidden", !presentationRecord.hasWarnings() && !presentationRecord.childHasWarnings());
+ this._warningElement.classList.toggle("timeline-tree-item-child-warning", presentationRecord.childHasWarnings() && !presentationRecord.hasWarnings());
+
+ if (presentationRecord.coalesced()) {
+ this._dataElement.createTextChild(WebInspector.UIString("× %d", presentationRecord.presentationChildren().length));
+ } else {
+ var detailsNode = uiUtils.buildDetailsNode(record, this._linkifier, loadedFromFile);
+ if (detailsNode) {
+ this._dataElement.appendChild(document.createTextNode("("));
+ this._dataElement.appendChild(detailsNode);
+ this._dataElement.appendChild(document.createTextNode(")"));
+ }
+ }
+
+ this._expandArrowElement.classList.toggle("parent", presentationRecord.expandable());
+ this._expandArrowElement.classList.toggle("expanded", !!presentationRecord.visibleChildrenCount());
+ this._record.setListRow(this);
+ },
+
+ highlight: function(regExp, domChanges)
+ {
+ var matchInfo = this.element.textContent.match(regExp);
+ if (matchInfo)
+ WebInspector.highlightSearchResult(this.element, matchInfo.index, matchInfo[0].length, domChanges);
+ },
+
+ dispose: function()
+ {
+ this.element.remove();
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _onExpandClick: function(event)
+ {
+ this._record.setCollapsed(!this._record.collapsed());
+ this._scheduleRefresh();
+ event.consume(true);
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _onClick: function(event)
+ {
+ this._selectRecord(this._record);
+ },
+
+ /**
+ * @param {boolean} selected
+ */
+ renderAsSelected: function(selected)
+ {
+ this.element.classList.toggle("selected", selected);
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _onMouseOver: function(event)
+ {
+ this.element.classList.add("hovered");
+ if (this._record.graphRow())
+ this._record.graphRow().element.classList.add("hovered");
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _onMouseOut: function(event)
+ {
+ this.element.classList.remove("hovered");
+ if (this._record.graphRow())
+ this._record.graphRow().element.classList.remove("hovered");
+ }
+}
+
+/**
+ * @constructor
+ * @param {!Element} graphContainer
+ * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
+ * @param {function()} scheduleRefresh
+ */
+WebInspector.TimelineRecordGraphRow = function(graphContainer, selectRecord, scheduleRefresh)
+{
+ this.element = document.createElement("div");
+ this.element.row = this;
+ this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
+ this.element.addEventListener("mouseout", this._onMouseOut.bind(this), false);
+ this.element.addEventListener("click", this._onClick.bind(this), false);
+
+ this._barAreaElement = document.createElement("div");
+ this._barAreaElement.className = "timeline-graph-bar-area";
+ this.element.appendChild(this._barAreaElement);
+
+ 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._selectRecord = selectRecord;
+ this._scheduleRefresh = scheduleRefresh;
+}
+
+WebInspector.TimelineRecordGraphRow.prototype = {
+ /**
+ * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
+ * @param {!WebInspector.TimelineCalculator} calculator
+ * @param {number} expandOffset
+ * @param {number} index
+ */
+ update: function(presentationRecord, calculator, expandOffset, index)
+ {
+ this._record = presentationRecord;
+ var record = presentationRecord.record();
+ this.element.className = "timeline-graph-side timeline-category-" + record.category().name;
+ if (record.thread())
+ this.element.classList.add("background");
+
+ var barPosition = calculator.computeBarGraphWindowPosition(presentationRecord);
+ this._barElement.style.left = barPosition.left + "px";
+ this._barElement.style.width = barPosition.width + "px";
+ this._barCpuElement.style.left = barPosition.left + "px";
+ this._barCpuElement.style.width = barPosition.cpuWidth + "px";
+ this._expandElement._update(presentationRecord, index, barPosition.left - expandOffset, barPosition.width);
+ this._record.setGraphRow(this);
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _onClick: function(event)
+ {
+ // check if we click arrow and expand if yes.
+ if (this._expandElement._arrow.containsEventPoint(event))
+ this._expand();
+ this._selectRecord(this._record);
+ },
+
+ /**
+ * @param {boolean} selected
+ */
+ renderAsSelected: function(selected)
+ {
+ this.element.classList.toggle("selected", selected);
+ },
+
+ _expand: function()
+ {
+ this._record.setCollapsed(!this._record.collapsed());
+ this._scheduleRefresh();
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _onMouseOver: function(event)
+ {
+ this.element.classList.add("hovered");
+ if (this._record.listRow())
+ this._record.listRow().element.classList.add("hovered");
+ },
+
+ /**
+ * @param {?Event} event
+ */
+ _onMouseOut: function(event)
+ {
+ this.element.classList.remove("hovered");
+ if (this._record.listRow())
+ this._record.listRow().element.classList.remove("hovered");
+ },
+
+ dispose: function()
+ {
+ this.element.remove();
+ this._expandElement._dispose();
+ }
+}
+
+/**
+ * @constructor
+ */
+WebInspector.TimelineExpandableElement = function(container)
+{
+ this._element = container.createChild("div", "timeline-expandable");
+ this._element.createChild("div", "timeline-expandable-left");
+ this._arrow = this._element.createChild("div", "timeline-expandable-arrow");
+}
+
+WebInspector.TimelineExpandableElement.prototype = {
+ /**
+ * @param {!WebInspector.TimelinePresentationModel.Record} record
+ * @param {number} index
+ * @param {number} left
+ * @param {number} width
+ */
+ _update: function(record, index, left, width)
+ {
+ const rowHeight = WebInspector.TimelinePanel.rowHeight;
+ if (record.visibleChildrenCount() || record.expandable()) {
+ this._element.style.top = index * rowHeight + "px";
+ this._element.style.left = left + "px";
+ this._element.style.width = Math.max(12, width + 25) + "px";
+ if (!record.collapsed()) {
+ this._element.style.height = (record.visibleChildrenCount() + 1) * rowHeight + "px";
+ this._element.classList.add("timeline-expandable-expanded");
+ this._element.classList.remove("timeline-expandable-collapsed");
+ } else {
+ this._element.style.height = rowHeight + "px";
+ this._element.classList.add("timeline-expandable-collapsed");
+ this._element.classList.remove("timeline-expandable-expanded");
+ }
+ this._element.classList.remove("hidden");
+ } else
+ this._element.classList.add("hidden");
+ },
+
+ _dispose: function()
+ {
+ this._element.remove();
+ }
+}