2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 * Copyright (C) 2012 Intel Inc. All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
9 * * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
15 * * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 * @extends {WebInspector.HBox}
35 * @implements {WebInspector.TimelineModeView}
36 * @param {!WebInspector.TimelineModeViewDelegate} delegate
37 * @param {!WebInspector.TimelineModel} model
38 * @param {!WebInspector.TimelineUIUtils} uiUtils
40 WebInspector.TimelineView = function(delegate, model, uiUtils)
42 WebInspector.HBox.call(this);
43 this.element.classList.add("timeline-view");
45 this._delegate = delegate;
47 this._uiUtils = uiUtils;
48 this._presentationModel = new WebInspector.TimelinePresentationModel(model, uiUtils);
49 this._calculator = new WebInspector.TimelineCalculator(model);
50 this._linkifier = new WebInspector.Linkifier();
51 this._frameStripByFrame = new Map();
53 this._boundariesAreValid = true;
56 this._recordsView = this._createRecordsView();
57 this._recordsView.addEventListener(WebInspector.SplitView.Events.SidebarSizeChanged, this._sidebarResized, this);
58 this._recordsView.show(this.element);
59 this._headerElement = this.element.createChild("div", "fill");
60 this._headerElement.id = "timeline-graph-records-header";
62 // Create gpu tasks containers.
63 this._cpuBarsElement = this._headerElement.createChild("div", "timeline-utilization-strip");
64 if (Runtime.experiments.isEnabled("gpuTimeline"))
65 this._gpuBarsElement = this._headerElement.createChild("div", "timeline-utilization-strip gpu");
67 this._popoverHelper = new WebInspector.PopoverHelper(this.element, this._getPopoverAnchor.bind(this), this._showPopover.bind(this));
69 this.element.addEventListener("mousemove", this._mouseMove.bind(this), false);
70 this.element.addEventListener("mouseleave", this._mouseLeave.bind(this), false);
71 this.element.addEventListener("keydown", this._keyDown.bind(this), false);
73 this._expandOffset = 15;
76 WebInspector.TimelineView.prototype = {
78 * @param {?WebInspector.TimelineFrameModelBase} frameModel
80 setFrameModel: function(frameModel)
82 this._frameModel = frameModel;
86 * @return {!WebInspector.SplitView}
88 _createRecordsView: function()
90 var recordsView = new WebInspector.SplitView(true, false, "timelinePanelRecorsSplitViewState");
91 this._containerElement = recordsView.element;
92 this._containerElement.tabIndex = 0;
93 this._containerElement.id = "timeline-container";
94 this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
96 // Create records list in the records sidebar.
97 recordsView.sidebarElement().createChild("div", "timeline-records-title").textContent = WebInspector.UIString("RECORDS");
98 this._sidebarListElement = recordsView.sidebarElement().createChild("div", "timeline-records-list");
100 // Create grid in the records main area.
101 this._gridContainer = new WebInspector.VBoxWithResizeCallback(this._onViewportResize.bind(this));
102 this._gridContainer.element.id = "resources-container-content";
103 this._gridContainer.show(recordsView.mainElement());
104 this._timelineGrid = new WebInspector.TimelineGrid();
105 this._gridContainer.element.appendChild(this._timelineGrid.element);
107 this._itemsGraphsElement = this._gridContainer.element.createChild("div");
108 this._itemsGraphsElement.id = "timeline-graphs";
110 // Create gap elements
111 this._topGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap");
112 this._graphRowsElement = this._itemsGraphsElement.createChild("div");
113 this._bottomGapElement = this._itemsGraphsElement.createChild("div", "timeline-gap");
114 this._expandElements = this._itemsGraphsElement.createChild("div");
115 this._expandElements.id = "orphan-expand-elements";
120 _rootRecord: function()
122 return this._presentationModel.rootRecord();
125 _updateEventDividers: function()
127 this._timelineGrid.removeEventDividers();
128 var clientWidth = this._graphRowsElementWidth;
130 var eventDividerRecords = this._model.eventDividerRecords();
132 for (var i = 0; i < eventDividerRecords.length; ++i) {
133 var record = eventDividerRecords[i];
134 var position = this._calculator.computePosition(record.startTime());
135 var dividerPosition = Math.round(position);
136 if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition])
138 var title = this._uiUtils.titleForRecord(record);
139 var divider = this._uiUtils.createEventDivider(record.type(), title);
140 divider.style.left = dividerPosition + "px";
141 dividers[dividerPosition] = divider;
143 this._timelineGrid.addEventDividers(dividers);
146 _updateFrameBars: function(frames)
148 var clientWidth = this._graphRowsElementWidth;
149 if (this._frameContainer) {
150 this._frameContainer.removeChildren();
152 const frameContainerBorderWidth = 1;
153 this._frameContainer = createElementWithClass("div", "fill timeline-frame-container");
154 this._frameContainer.style.height = WebInspector.TimelinePanel.rowHeight + frameContainerBorderWidth + "px";
155 this._frameContainer.addEventListener("dblclick", this._onFrameDoubleClicked.bind(this), false);
156 this._frameContainer.addEventListener("click", this._onFrameClicked.bind(this), false);
158 this._frameStripByFrame.clear();
162 for (var i = 0; i < frames.length; ++i) {
163 var frame = frames[i];
164 var frameStart = this._calculator.computePosition(frame.startTime);
165 var frameEnd = this._calculator.computePosition(frame.endTime);
167 var frameStrip = createElementWithClass("div", "timeline-frame-strip");
168 var actualStart = Math.max(frameStart, 0);
169 var width = frameEnd - actualStart;
170 frameStrip.style.left = actualStart + "px";
171 frameStrip.style.width = width + "px";
172 frameStrip._frame = frame;
173 this._frameStripByFrame.set(frame, frameStrip);
175 const minWidthForFrameInfo = 60;
176 if (width > minWidthForFrameInfo)
177 frameStrip.textContent = Number.millisToString(frame.endTime - frame.startTime, true);
179 this._frameContainer.appendChild(frameStrip);
181 if (actualStart > 0) {
182 var frameMarker = this._uiUtils.createBeginFrameDivider();
183 frameMarker.style.left = frameStart + "px";
184 dividers.push(frameMarker);
187 this._timelineGrid.addEventDividers(dividers);
188 this._headerElement.appendChild(this._frameContainer);
191 _onFrameDoubleClicked: function(event)
193 var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip");
196 this._delegate.requestWindowTimes(frameBar._frame.startTime, frameBar._frame.endTime);
199 _onFrameClicked: function(event)
201 var frameBar = event.target.enclosingNodeOrSelfWithClass("timeline-frame-strip");
204 this._delegate.select(WebInspector.TimelineSelection.fromFrame(frameBar._frame));
208 * @param {!WebInspector.TimelineModel.Record} record
210 addRecord: function(record)
212 this._presentationModel.addRecord(record);
213 this._invalidateAndScheduleRefresh(false, false);
217 * @param {number} width
219 setSidebarSize: function(width)
221 this._recordsView.setSidebarSize(width);
225 * @param {!WebInspector.Event} event
227 _sidebarResized: function(event)
229 this.dispatchEventToListeners(WebInspector.SplitView.Events.SidebarSizeChanged, event.data);
232 _onViewportResize: function()
234 this._resize(this._recordsView.sidebarSize());
238 * @param {number} sidebarWidth
240 _resize: function(sidebarWidth)
242 this._closeRecordDetails();
243 this._graphRowsElementWidth = this._graphRowsElement.offsetWidth;
244 this._headerElement.style.left = sidebarWidth + "px";
245 this._headerElement.style.width = this._itemsGraphsElement.offsetWidth + "px";
246 this._scheduleRefresh(false, true);
249 _resetView: function()
251 this._windowStartTime = 0;
252 this._windowEndTime = 0;
253 this._boundariesAreValid = false;
254 this._adjustScrollPosition(0);
255 this._linkifier.reset();
256 this._closeRecordDetails();
257 this._automaticallySizeWindow = true;
258 this._presentationModel.reset();
263 * @return {!WebInspector.View}
277 this._invalidateAndScheduleRefresh(true, true);
281 * @return {!Array.<!Element>}
283 elementsToRestoreScrollPositionsFor: function()
285 return [this._containerElement];
289 * @param {?RegExp} textFilter
291 refreshRecords: function(textFilter)
293 this._automaticallySizeWindow = false;
294 this._presentationModel.setTextFilter(textFilter);
295 this._invalidateAndScheduleRefresh(false, true);
300 this._closeRecordDetails();
301 WebInspector.View.prototype.willHide.call(this);
306 this._presentationModel.refreshRecords();
307 WebInspector.HBox.prototype.wasShown.call(this);
310 _onScroll: function(event)
312 this._closeRecordDetails();
313 this._scrollTop = this._containerElement.scrollTop;
314 var dividersTop = Math.max(0, this._scrollTop);
315 this._timelineGrid.setScrollAndDividerTop(this._scrollTop, dividersTop);
316 this._scheduleRefresh(true, true);
320 * @param {boolean} preserveBoundaries
321 * @param {boolean} userGesture
323 _invalidateAndScheduleRefresh: function(preserveBoundaries, userGesture)
325 this._presentationModel.invalidateFilteredRecords();
326 this._scheduleRefresh(preserveBoundaries, userGesture);
329 _clearSelection: function()
331 this._delegate.select(null);
335 * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord
337 _selectRecord: function(presentationRecord)
339 if (presentationRecord.coalesced()) {
340 // Presentation record does not have model record to highlight.
341 this._innerSetSelectedRecord(presentationRecord);
342 var aggregatedStats = {};
343 var presentationChildren = presentationRecord.presentationChildren();
344 for (var i = 0; i < presentationChildren.length; ++i)
345 this._uiUtils.aggregateTimeForRecord(aggregatedStats, presentationChildren[i].record());
346 var idle = presentationRecord.endTime() - presentationRecord.startTime();
347 for (var category in aggregatedStats)
348 idle -= aggregatedStats[category];
349 aggregatedStats["idle"] = idle;
351 var contentHelper = new WebInspector.TimelineDetailsContentHelper(null, null, true);
352 var pieChart = WebInspector.TimelineUIUtils.generatePieChart(aggregatedStats);
353 var title = this._uiUtils.titleForRecord(presentationRecord.record());
354 contentHelper.appendTextRow(WebInspector.UIString("Type"), title);
355 contentHelper.appendElementRow(WebInspector.UIString("Aggregated Time"), pieChart);
356 this._delegate.showInDetails(contentHelper.element);
359 this._delegate.select(WebInspector.TimelineSelection.fromRecord(presentationRecord.record()));
363 * @param {?WebInspector.TimelineSelection} selection
365 setSelection: function(selection)
368 this._innerSetSelectedRecord(null);
369 this._setSelectedFrame(null);
372 if (selection.type() === WebInspector.TimelineSelection.Type.Record) {
373 var record = /** @type {!WebInspector.TimelineModel.Record} */ (selection.object());
374 this._innerSetSelectedRecord(this._presentationModel.toPresentationRecord(record));
375 this._setSelectedFrame(null);
376 } else if (selection.type() === WebInspector.TimelineSelection.Type.Frame) {
377 var frame = /** @type {!WebInspector.TimelineFrame} */ (selection.object());
378 this._innerSetSelectedRecord(null);
379 this._setSelectedFrame(frame);
384 * @param {?WebInspector.TimelinePresentationModel.Record} presentationRecord
386 _innerSetSelectedRecord: function(presentationRecord)
388 if (presentationRecord === this._lastSelectedRecord)
391 // Remove selection rendering.p
392 if (this._lastSelectedRecord) {
393 if (this._lastSelectedRecord.listRow())
394 this._lastSelectedRecord.listRow().renderAsSelected(false);
395 if (this._lastSelectedRecord.graphRow())
396 this._lastSelectedRecord.graphRow().renderAsSelected(false);
399 this._lastSelectedRecord = presentationRecord;
400 if (!presentationRecord)
403 this._innerRevealRecord(presentationRecord);
404 if (presentationRecord.listRow())
405 presentationRecord.listRow().renderAsSelected(true);
406 if (presentationRecord.graphRow())
407 presentationRecord.graphRow().renderAsSelected(true);
411 * @param {?WebInspector.TimelineFrame} frame
413 _setSelectedFrame: function(frame)
415 if (this._lastSelectedFrame === frame)
417 var oldStripElement = this._lastSelectedFrame && this._frameStripByFrame.get(this._lastSelectedFrame);
419 oldStripElement.classList.remove("selected");
420 var newStripElement = frame && this._frameStripByFrame.get(frame);
422 newStripElement.classList.add("selected");
423 this._lastSelectedFrame = frame;
427 * @param {number} startTime
428 * @param {number} endTime
430 setWindowTimes: function(startTime, endTime)
432 this._windowStartTime = startTime;
433 this._windowEndTime = endTime;
434 this._presentationModel.setWindowTimes(startTime, endTime);
435 this._automaticallySizeWindow = false;
436 this._invalidateAndScheduleRefresh(false, true);
437 this._clearSelection();
441 * @param {boolean} preserveBoundaries
442 * @param {boolean} userGesture
444 _scheduleRefresh: function(preserveBoundaries, userGesture)
446 this._closeRecordDetails();
447 this._boundariesAreValid &= preserveBoundaries;
449 if (!this.isShowing())
452 if (preserveBoundaries || userGesture)
455 if (!this._refreshTimeout)
456 this._refreshTimeout = setTimeout(this._refresh.bind(this), 300);
462 if (this._refreshTimeout) {
463 clearTimeout(this._refreshTimeout);
464 delete this._refreshTimeout;
466 var windowStartTime = this._windowStartTime || this._model.minimumRecordTime();
467 var windowEndTime = this._windowEndTime || this._model.maximumRecordTime();
468 this._timelinePaddingLeft = this._expandOffset;
469 this._calculator.setWindow(windowStartTime, windowEndTime);
470 this._calculator.setDisplayWindow(this._timelinePaddingLeft, this._graphRowsElementWidth);
472 this._refreshRecords();
473 if (!this._boundariesAreValid) {
474 this._updateEventDividers();
475 if (this._frameContainer)
476 this._frameContainer.remove();
477 if (this._frameModel) {
478 var frames = this._frameModel.filteredFrames(windowStartTime, windowEndTime);
479 const maxFramesForFrameBars = 30;
480 if (frames.length && frames.length < maxFramesForFrameBars) {
481 this._timelineGrid.removeDividers();
482 this._updateFrameBars(frames);
484 this._timelineGrid.updateDividers(this._calculator);
487 this._timelineGrid.updateDividers(this._calculator);
488 this._refreshAllUtilizationBars();
490 this._boundariesAreValid = true;
494 * @param {!WebInspector.TimelinePresentationModel.Record} recordToReveal
496 _innerRevealRecord: function(recordToReveal)
498 var needRefresh = false;
499 // Expand all ancestors.
500 for (var parent = recordToReveal.presentationParent(); parent !== this._rootRecord(); parent = parent.presentationParent()) {
501 if (!parent.collapsed())
503 this._presentationModel.invalidateFilteredRecords();
504 parent.setCollapsed(false);
507 var recordsInWindow = this._presentationModel.filteredRecords();
508 var index = recordsInWindow.indexOf(recordToReveal);
510 var itemOffset = index * WebInspector.TimelinePanel.rowHeight;
511 var visibleTop = this._scrollTop - WebInspector.TimelinePanel.headerHeight;
512 var visibleBottom = visibleTop + this._containerElementHeight - WebInspector.TimelinePanel.rowHeight;
513 if (itemOffset < visibleTop)
514 this._containerElement.scrollTop = itemOffset;
515 else if (itemOffset > visibleBottom)
516 this._containerElement.scrollTop = itemOffset - this._containerElementHeight + WebInspector.TimelinePanel.headerHeight + WebInspector.TimelinePanel.rowHeight;
517 else if (needRefresh)
518 this._refreshRecords();
521 _refreshRecords: function()
523 this._containerElementHeight = this._containerElement.clientHeight;
524 var recordsInWindow = this._presentationModel.filteredRecords();
526 // Calculate the visible area.
527 var visibleTop = this._scrollTop;
528 var visibleBottom = visibleTop + this._containerElementHeight;
530 var rowHeight = WebInspector.TimelinePanel.rowHeight;
531 var headerHeight = WebInspector.TimelinePanel.headerHeight;
533 // Convert visible area to visible indexes. Always include top-level record for a visible nested record.
534 var startIndex = Math.max(0, Math.min(Math.floor((visibleTop - headerHeight) / rowHeight), recordsInWindow.length - 1));
535 var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
536 var lastVisibleLine = Math.max(0, Math.floor((visibleBottom - headerHeight) / rowHeight));
537 if (this._automaticallySizeWindow && recordsInWindow.length > lastVisibleLine) {
538 this._automaticallySizeWindow = false;
539 this._clearSelection();
540 // If we're at the top, always use real timeline start as a left window bound so that expansion arrow padding logic works.
541 var windowStartTime = startIndex ? recordsInWindow[startIndex].startTime() : this._model.minimumRecordTime();
542 var windowEndTime = recordsInWindow[Math.max(0, lastVisibleLine - 1)].endTime();
543 this._delegate.requestWindowTimes(windowStartTime, windowEndTime);
544 recordsInWindow = this._presentationModel.filteredRecords();
545 endIndex = Math.min(recordsInWindow.length, lastVisibleLine);
548 // Resize gaps first.
549 this._topGapElement.style.height = (startIndex * rowHeight) + "px";
550 this._recordsView.sidebarElement().firstElementChild.style.flexBasis = (startIndex * rowHeight + headerHeight) + "px";
551 this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
552 var rowsHeight = headerHeight + recordsInWindow.length * rowHeight;
553 var totalHeight = Math.max(this._containerElementHeight, rowsHeight);
555 this._recordsView.mainElement().style.height = totalHeight + "px";
556 this._recordsView.sidebarElement().style.height = totalHeight + "px";
557 this._recordsView.resizerElement().style.height = totalHeight + "px";
559 // Update visible rows.
560 var listRowElement = this._sidebarListElement.firstChild;
561 var width = this._graphRowsElementWidth;
562 this._itemsGraphsElement.removeChild(this._graphRowsElement);
563 var graphRowElement = this._graphRowsElement.firstChild;
564 var scheduleRefreshCallback = this._invalidateAndScheduleRefresh.bind(this, true, true);
565 var selectRecordCallback = this._selectRecord.bind(this);
566 this._itemsGraphsElement.removeChild(this._expandElements);
567 this._expandElements.removeChildren();
569 for (var i = 0; i < endIndex; ++i) {
570 var record = recordsInWindow[i];
572 if (i < startIndex) {
573 var lastChildIndex = i + record.visibleChildrenCount();
574 if (lastChildIndex >= startIndex && lastChildIndex < endIndex) {
575 var expandElement = new WebInspector.TimelineExpandableElement(this._expandElements);
576 var positions = this._calculator.computeBarGraphWindowPosition(record);
577 expandElement._update(record, i, positions.left - this._expandOffset, positions.width);
580 if (!listRowElement) {
581 listRowElement = new WebInspector.TimelineRecordListRow(this._linkifier, selectRecordCallback, scheduleRefreshCallback).element;
582 this._sidebarListElement.appendChild(listRowElement);
584 if (!graphRowElement) {
585 graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, selectRecordCallback, scheduleRefreshCallback).element;
586 this._graphRowsElement.appendChild(graphRowElement);
589 listRowElement.row.update(record, visibleTop, this._uiUtils);
590 graphRowElement.row.update(record, this._calculator, this._expandOffset, i, this._uiUtils);
591 if (this._lastSelectedRecord === record) {
592 listRowElement.row.renderAsSelected(true);
593 graphRowElement.row.renderAsSelected(true);
596 listRowElement = listRowElement.nextSibling;
597 graphRowElement = graphRowElement.nextSibling;
601 // Remove extra rows.
602 while (listRowElement) {
603 var nextElement = listRowElement.nextSibling;
604 listRowElement.row.dispose();
605 listRowElement = nextElement;
607 while (graphRowElement) {
608 var nextElement = graphRowElement.nextSibling;
609 graphRowElement.row.dispose();
610 graphRowElement = nextElement;
613 this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
614 this._itemsGraphsElement.appendChild(this._expandElements);
615 this._adjustScrollPosition(recordsInWindow.length * rowHeight + headerHeight);
617 return recordsInWindow.length;
620 _refreshAllUtilizationBars: function()
622 this._refreshUtilizationBars(WebInspector.UIString("CPU"), this._model.mainThreadTasks(), this._cpuBarsElement);
623 if (Runtime.experiments.isEnabled("gpuTimeline"))
624 this._refreshUtilizationBars(WebInspector.UIString("GPU"), this._model.gpuThreadTasks(), this._gpuBarsElement);
628 * @param {string} name
629 * @param {!Array.<!WebInspector.TimelineModel.Record>} tasks
630 * @param {?Element} container
632 _refreshUtilizationBars: function(name, tasks, container)
640 var minWidth = WebInspector.TimelineCalculator._minWidth;
641 var widthAdjustment = minWidth / 2;
643 var width = this._graphRowsElementWidth;
644 var boundarySpan = this._windowEndTime - this._windowStartTime;
645 var scale = boundarySpan / (width - minWidth - this._timelinePaddingLeft);
646 var startTime = (this._windowStartTime - this._timelinePaddingLeft * scale);
647 var endTime = startTime + width * scale;
650 * @param {number} value
651 * @param {!WebInspector.TimelineModel.Record} task
654 function compareEndTime(value, task)
656 return value < task.endTime() ? -1 : 1;
659 var taskIndex = insertionIndexForObjectInListSortedByFunction(startTime, tasks, compareEndTime);
661 var foreignStyle = "gpu-task-foreign";
662 var element = /** @type {?Element} */ (container.firstChild);
667 for (; taskIndex < tasks.length; ++taskIndex) {
668 var task = tasks[taskIndex];
669 if (task.startTime() > endTime)
672 var left = Math.max(0, this._calculator.computePosition(task.startTime()) + barOffset - widthAdjustment);
673 var right = Math.min(width, this._calculator.computePosition(task.endTime() || 0) + barOffset + widthAdjustment);
676 var gap = Math.floor(left) - Math.ceil(lastRight);
678 if (!task.data["foreign"])
679 lastElement.classList.remove(foreignStyle);
681 lastElement._tasksInfo.lastTaskIndex = taskIndex;
684 lastElement.style.width = (lastRight - lastLeft) + "px";
688 element = container.createChild("div", "timeline-graph-bar");
689 element.style.left = left + "px";
690 element._tasksInfo = {name: name, tasks: tasks, firstTaskIndex: taskIndex, lastTaskIndex: taskIndex};
691 if (task.data["foreign"])
692 element.classList.add(foreignStyle);
695 lastElement = element;
696 element = /** @type {?Element} */ (element.nextSibling);
700 lastElement.style.width = (lastRight - lastLeft) + "px";
703 var nextElement = element.nextSibling;
704 element._tasksInfo = null;
705 container.removeChild(element);
706 element = nextElement;
710 _adjustScrollPosition: function(totalHeight)
712 // Prevent the container from being scrolled off the end.
713 if ((this._scrollTop + this._containerElementHeight) > totalHeight + 1)
714 this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
718 * @param {!Element} element
719 * @param {!Event} event
720 * @return {!Element|!AnchorBox|undefined}
722 _getPopoverAnchor: function(element, event)
724 var anchor = element.enclosingNodeOrSelfWithClass("timeline-graph-bar");
725 if (anchor && anchor._tasksInfo)
729 _mouseLeave: function()
731 this._hideQuadHighlight();
737 _mouseMove: function(e)
739 var rowElement = e.target.enclosingNodeOrSelfWithClass("timeline-tree-item");
740 if (!this._highlightQuad(rowElement))
741 this._hideQuadHighlight();
743 var taskBarElement = e.target.enclosingNodeOrSelfWithClass("timeline-graph-bar");
744 if (taskBarElement && taskBarElement._tasksInfo) {
745 var offset = taskBarElement.offsetLeft;
746 this._timelineGrid.showCurtains(offset >= 0 ? offset : 0, taskBarElement.offsetWidth);
748 this._timelineGrid.hideCurtains();
752 * @param {!Event} event
754 _keyDown: function(event)
756 if (!this._lastSelectedRecord || event.shiftKey || event.metaKey || event.ctrlKey)
759 var record = this._lastSelectedRecord;
760 var recordsInWindow = this._presentationModel.filteredRecords();
761 var index = recordsInWindow.indexOf(record);
762 var recordsInPage = Math.floor(this._containerElementHeight / WebInspector.TimelinePanel.rowHeight);
763 var rowHeight = WebInspector.TimelinePanel.rowHeight;
768 switch (event.keyIdentifier) {
770 if (record.presentationParent()) {
771 if ((!record.expandable() || record.collapsed()) && record.presentationParent() !== this._presentationModel.rootRecord()) {
772 this._selectRecord(record.presentationParent());
774 record.setCollapsed(true);
775 this._invalidateAndScheduleRefresh(true, true);
783 this._selectRecord(recordsInWindow[index]);
787 if (record.expandable() && record.collapsed()) {
788 record.setCollapsed(false);
789 this._invalidateAndScheduleRefresh(true, true);
791 if (++index >= recordsInWindow.length)
793 this._selectRecord(recordsInWindow[index]);
798 if (++index >= recordsInWindow.length)
800 this._selectRecord(recordsInWindow[index]);
804 index = Math.max(0, index - recordsInPage);
805 this._scrollTop = Math.max(0, this._scrollTop - recordsInPage * rowHeight);
806 this._containerElement.scrollTop = this._scrollTop;
807 this._selectRecord(recordsInWindow[index]);
811 index = Math.min(recordsInWindow.length - 1, index + recordsInPage);
812 this._scrollTop = Math.min(this._containerElement.scrollHeight - this._containerElementHeight, this._scrollTop + recordsInPage * rowHeight);
813 this._containerElement.scrollTop = this._scrollTop;
814 this._selectRecord(recordsInWindow[index]);
819 this._selectRecord(recordsInWindow[index]);
823 index = recordsInWindow.length - 1;
824 this._selectRecord(recordsInWindow[index]);
831 * @param {?Element} rowElement
834 _highlightQuad: function(rowElement)
836 if (!rowElement || !rowElement.row)
838 var presentationRecord = rowElement.row._record;
839 if (presentationRecord.coalesced())
841 var record = presentationRecord.record();
842 if (this._highlightedQuadRecord === record)
845 var quad = this._uiUtils.highlightQuadForRecord(record);
846 var target = record.target();
847 if (!quad || !target)
849 this._highlightedQuadRecord = record;
850 target.domAgent().highlightQuad(quad, WebInspector.Color.PageHighlight.Content.toProtocolRGBA(), WebInspector.Color.PageHighlight.ContentOutline.toProtocolRGBA());
854 _hideQuadHighlight: function()
856 var target = this._highlightedQuadRecord ? this._highlightedQuadRecord.target() : null;
858 target.domAgent().hideHighlight();
860 if (this._highlightedQuadRecord)
861 delete this._highlightedQuadRecord;
865 * @param {!Element} anchor
866 * @param {!WebInspector.Popover} popover
868 _showPopover: function(anchor, popover)
870 if (!anchor._tasksInfo)
872 popover.show(WebInspector.TimelineUIUtils.generateMainThreadBarPopupContent(this._model, anchor._tasksInfo), anchor, null, null, WebInspector.Popover.Orientation.Bottom);
875 _closeRecordDetails: function()
877 this._popoverHelper.hidePopover();
881 * @param {?WebInspector.TimelineModel.Record} record
882 * @param {string=} regex
883 * @param {boolean=} selectRecord
885 highlightSearchResult: function(record, regex, selectRecord)
887 if (this._highlightDomChanges)
888 WebInspector.revertDomChanges(this._highlightDomChanges);
889 this._highlightDomChanges = [];
891 var presentationRecord = this._presentationModel.toPresentationRecord(record);
892 if (!presentationRecord)
896 this._selectRecord(presentationRecord);
898 for (var element = this._sidebarListElement.firstChild; element; element = element.nextSibling) {
899 if (element.row._record === presentationRecord) {
900 element.row.highlight(regex, this._highlightDomChanges);
906 __proto__: WebInspector.HBox.prototype
911 * @param {!WebInspector.TimelineModel} model
912 * @implements {WebInspector.TimelineGrid.Calculator}
914 WebInspector.TimelineCalculator = function(model)
919 WebInspector.TimelineCalculator._minWidth = 5;
921 WebInspector.TimelineCalculator.prototype = {
925 paddingLeft: function()
927 return this._paddingLeft;
931 * @param {number} time
934 computePosition: function(time)
936 return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this._paddingLeft;
940 * @param {!WebInspector.TimelinePresentationModel.Record} record
941 * @return {!{start: number, end: number, cpuWidth: number}}
943 computeBarGraphPercentages: function(record)
945 var start = (record.startTime() - this._minimumBoundary) / this.boundarySpan() * 100;
946 var end = (record.startTime() + record.selfTime() - this._minimumBoundary) / this.boundarySpan() * 100;
947 var cpuWidth = (record.endTime() - record.startTime()) / this.boundarySpan() * 100;
948 return {start: start, end: end, cpuWidth: cpuWidth};
952 * @param {!WebInspector.TimelinePresentationModel.Record} record
953 * @return {!{left: number, width: number, cpuWidth: number}}
955 computeBarGraphWindowPosition: function(record)
957 var percentages = this.computeBarGraphPercentages(record);
958 var widthAdjustment = 0;
960 var left = this.computePosition(record.startTime());
961 var width = (percentages.end - percentages.start) / 100 * this._workingArea;
962 if (width < WebInspector.TimelineCalculator._minWidth) {
963 widthAdjustment = WebInspector.TimelineCalculator._minWidth - width;
964 width = WebInspector.TimelineCalculator._minWidth;
966 var cpuWidth = percentages.cpuWidth / 100 * this._workingArea + widthAdjustment;
967 return {left: left, width: width, cpuWidth: cpuWidth};
970 setWindow: function(minimumBoundary, maximumBoundary)
972 this._minimumBoundary = minimumBoundary;
973 this._maximumBoundary = maximumBoundary;
977 * @param {number} paddingLeft
978 * @param {number} clientWidth
980 setDisplayWindow: function(paddingLeft, clientWidth)
982 this._workingArea = clientWidth - WebInspector.TimelineCalculator._minWidth - paddingLeft;
983 this._paddingLeft = paddingLeft;
987 * @param {number} value
988 * @param {number=} precision
991 formatTime: function(value, precision)
993 return Number.preciseMillisToString(value - this.zeroTime(), precision);
999 maximumBoundary: function()
1001 return this._maximumBoundary;
1007 minimumBoundary: function()
1009 return this._minimumBoundary;
1015 zeroTime: function()
1017 return this._model.minimumRecordTime();
1023 boundarySpan: function()
1025 return this._maximumBoundary - this._minimumBoundary;
1031 * @param {!WebInspector.Linkifier} linkifier
1032 * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
1033 * @param {function()} scheduleRefresh
1035 WebInspector.TimelineRecordListRow = function(linkifier, selectRecord, scheduleRefresh)
1037 this.element = createElement("div");
1038 this.element.row = this;
1039 this.element.style.cursor = "pointer";
1040 this.element.addEventListener("click", this._onClick.bind(this), false);
1041 this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
1042 this.element.addEventListener("mouseleave", this._onMouseLeave.bind(this), false);
1043 this._linkifier = linkifier;
1045 // Warning is float right block, it goes first.
1046 this._warningElement = this.element.createChild("div", "timeline-tree-item-warning hidden");
1048 this._expandArrowElement = this.element.createChild("div", "timeline-tree-item-expand-arrow");
1049 this._expandArrowElement.addEventListener("click", this._onExpandClick.bind(this), false);
1050 var iconElement = this.element.createChild("span", "timeline-tree-icon");
1051 this._typeElement = this.element.createChild("span", "type");
1053 this._dataElement = this.element.createChild("span", "data dimmed");
1054 this._scheduleRefresh = scheduleRefresh;
1055 this._selectRecord = selectRecord;
1058 WebInspector.TimelineRecordListRow.prototype = {
1060 * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
1061 * @param {number} offset
1062 * @param {!WebInspector.TimelineUIUtils} uiUtils
1064 update: function(presentationRecord, offset, uiUtils)
1066 this._record = presentationRecord;
1067 var record = presentationRecord.record();
1068 this._offset = offset;
1070 this.element.className = "timeline-tree-item timeline-category-" + uiUtils.categoryForRecord(record).name;
1071 var paddingLeft = 5;
1073 for (var currentRecord = presentationRecord.presentationParent() ? presentationRecord.presentationParent().presentationParent() : null; currentRecord; currentRecord = currentRecord.presentationParent())
1074 paddingLeft += 12 / (Math.max(1, step++));
1075 this.element.style.paddingLeft = paddingLeft + "px";
1076 if (record.thread() !== WebInspector.TimelineModel.MainThreadName)
1077 this.element.classList.add("background");
1079 this._typeElement.textContent = uiUtils.titleForRecord(record);
1081 if (this._dataElement.firstChild)
1082 this._dataElement.removeChildren();
1084 this._warningElement.classList.toggle("hidden", !presentationRecord.hasWarnings() && !presentationRecord.childHasWarnings());
1085 this._warningElement.classList.toggle("timeline-tree-item-child-warning", presentationRecord.childHasWarnings() && !presentationRecord.hasWarnings());
1087 if (presentationRecord.coalesced()) {
1088 this._dataElement.createTextChild(WebInspector.UIString("× %d", presentationRecord.presentationChildren().length));
1090 var detailsNode = uiUtils.buildDetailsNode(record, this._linkifier);
1092 this._dataElement.createTextChild("(");
1093 this._dataElement.appendChild(detailsNode);
1094 this._dataElement.createTextChild(")");
1098 this._expandArrowElement.classList.toggle("parent", presentationRecord.expandable());
1099 this._expandArrowElement.classList.toggle("expanded", !!presentationRecord.visibleChildrenCount());
1100 this._record.setListRow(this);
1103 highlight: function(regExp, domChanges)
1105 var matchInfo = this.element.textContent.match(regExp);
1107 WebInspector.highlightSearchResult(this.element, matchInfo.index, matchInfo[0].length, domChanges);
1112 this.element.remove();
1116 * @param {!Event} event
1118 _onExpandClick: function(event)
1120 this._record.setCollapsed(!this._record.collapsed());
1121 this._scheduleRefresh();
1122 event.consume(true);
1126 * @param {!Event} event
1128 _onClick: function(event)
1130 this._selectRecord(this._record);
1134 * @param {boolean} selected
1136 renderAsSelected: function(selected)
1138 this.element.classList.toggle("selected", selected);
1142 * @param {!Event} event
1144 _onMouseOver: function(event)
1146 this.element.classList.add("hovered");
1147 if (this._record.graphRow())
1148 this._record.graphRow().element.classList.add("hovered");
1152 * @param {!Event} event
1154 _onMouseLeave: function(event)
1156 this.element.classList.remove("hovered");
1157 if (this._record.graphRow())
1158 this._record.graphRow().element.classList.remove("hovered");
1164 * @param {!Element} graphContainer
1165 * @param {function(!WebInspector.TimelinePresentationModel.Record)} selectRecord
1166 * @param {function()} scheduleRefresh
1168 WebInspector.TimelineRecordGraphRow = function(graphContainer, selectRecord, scheduleRefresh)
1170 this.element = createElement("div");
1171 this.element.row = this;
1172 this.element.addEventListener("mouseover", this._onMouseOver.bind(this), false);
1173 this.element.addEventListener("mouseleave", this._onMouseLeave.bind(this), false);
1174 this.element.addEventListener("click", this._onClick.bind(this), false);
1176 this._barAreaElement = this.element.createChild("div", "timeline-graph-bar-area");
1178 this._barCpuElement = this._barAreaElement.createChild("div", "timeline-graph-bar cpu");
1179 this._barCpuElement.row = this;
1181 this._barElement = this._barAreaElement.createChild("div", "timeline-graph-bar");
1182 this._barElement.row = this;
1184 this._expandElement = new WebInspector.TimelineExpandableElement(graphContainer);
1186 this._selectRecord = selectRecord;
1187 this._scheduleRefresh = scheduleRefresh;
1190 WebInspector.TimelineRecordGraphRow.prototype = {
1192 * @param {!WebInspector.TimelinePresentationModel.Record} presentationRecord
1193 * @param {!WebInspector.TimelineCalculator} calculator
1194 * @param {number} expandOffset
1195 * @param {number} index
1196 * @param {!WebInspector.TimelineUIUtils} uiUtils
1198 update: function(presentationRecord, calculator, expandOffset, index, uiUtils)
1200 this._record = presentationRecord;
1201 var record = presentationRecord.record();
1202 this.element.className = "timeline-graph-side timeline-category-" + uiUtils.categoryForRecord(record).name;
1203 if (record.thread() !== WebInspector.TimelineModel.MainThreadName)
1204 this.element.classList.add("background");
1206 var barPosition = calculator.computeBarGraphWindowPosition(presentationRecord);
1207 this._barElement.style.left = barPosition.left + "px";
1208 this._barElement.style.width = barPosition.width + "px";
1209 this._barCpuElement.style.left = barPosition.left + "px";
1210 this._barCpuElement.style.width = barPosition.cpuWidth + "px";
1211 this._expandElement._update(presentationRecord, index, barPosition.left - expandOffset, barPosition.width);
1212 this._record.setGraphRow(this);
1216 * @param {!Event} event
1218 _onClick: function(event)
1220 // check if we click arrow and expand if yes.
1221 if (this._expandElement._arrow.containsEventPoint(event))
1223 this._selectRecord(this._record);
1227 * @param {boolean} selected
1229 renderAsSelected: function(selected)
1231 this.element.classList.toggle("selected", selected);
1236 this._record.setCollapsed(!this._record.collapsed());
1237 this._scheduleRefresh();
1241 * @param {!Event} event
1243 _onMouseOver: function(event)
1245 this.element.classList.add("hovered");
1246 if (this._record.listRow())
1247 this._record.listRow().element.classList.add("hovered");
1251 * @param {!Event} event
1253 _onMouseLeave: function(event)
1255 this.element.classList.remove("hovered");
1256 if (this._record.listRow())
1257 this._record.listRow().element.classList.remove("hovered");
1262 this.element.remove();
1263 this._expandElement._dispose();
1270 WebInspector.TimelineExpandableElement = function(container)
1272 this._element = container.createChild("div", "timeline-expandable");
1273 this._element.createChild("div", "timeline-expandable-left");
1274 this._arrow = this._element.createChild("div", "timeline-expandable-arrow");
1277 WebInspector.TimelineExpandableElement.prototype = {
1279 * @param {!WebInspector.TimelinePresentationModel.Record} record
1280 * @param {number} index
1281 * @param {number} left
1282 * @param {number} width
1284 _update: function(record, index, left, width)
1286 const rowHeight = WebInspector.TimelinePanel.rowHeight;
1287 if (record.visibleChildrenCount() || record.expandable()) {
1288 this._element.style.top = index * rowHeight + "px";
1289 this._element.style.left = left + "px";
1290 this._element.style.width = Math.max(12, width + 25) + "px";
1291 if (!record.collapsed()) {
1292 this._element.style.height = (record.visibleChildrenCount() + 1) * rowHeight + "px";
1293 this._element.classList.add("timeline-expandable-expanded");
1294 this._element.classList.remove("timeline-expandable-collapsed");
1296 this._element.style.height = rowHeight + "px";
1297 this._element.classList.add("timeline-expandable-collapsed");
1298 this._element.classList.remove("timeline-expandable-expanded");
1300 this._element.classList.remove("hidden");
1302 this._element.classList.add("hidden");
1306 _dispose: function()
1308 this._element.remove();