2 * Copyright (C) 2009 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 WebInspector.TimelinePanel = function()
33 WebInspector.Panel.call(this, "timeline");
35 this.element.appendChild(this._createTopPane());
36 this.element.tabIndex = 0;
38 this._sidebarBackgroundElement = document.createElement("div");
39 this._sidebarBackgroundElement.className = "sidebar timeline-sidebar-background";
40 this.element.appendChild(this._sidebarBackgroundElement);
42 this._containerElement = document.createElement("div");
43 this._containerElement.id = "timeline-container";
44 this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
45 this.element.appendChild(this._containerElement);
47 this.createSidebar(this._containerElement, this._containerElement);
48 var itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
49 itemsTreeElement.expanded = true;
50 this.sidebarTree.appendChild(itemsTreeElement);
52 this._sidebarListElement = document.createElement("div");
53 this.sidebarElement.appendChild(this._sidebarListElement);
55 this._containerContentElement = document.createElement("div");
56 this._containerContentElement.id = "resources-container-content";
57 this._containerElement.appendChild(this._containerContentElement);
59 this._timelineGrid = new WebInspector.TimelineGrid();
60 this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
61 this._itemsGraphsElement.id = "timeline-graphs";
62 this._containerContentElement.appendChild(this._timelineGrid.element);
64 this._topGapElement = document.createElement("div");
65 this._topGapElement.className = "timeline-gap";
66 this._itemsGraphsElement.appendChild(this._topGapElement);
68 this._graphRowsElement = document.createElement("div");
69 this._itemsGraphsElement.appendChild(this._graphRowsElement);
71 this._bottomGapElement = document.createElement("div");
72 this._bottomGapElement.className = "timeline-gap";
73 this._itemsGraphsElement.appendChild(this._bottomGapElement);
75 this._expandElements = document.createElement("div");
76 this._expandElements.id = "orphan-expand-elements";
77 this._itemsGraphsElement.appendChild(this._expandElements);
79 this._rootRecord = this._createRootRecord();
80 this._sendRequestRecords = {};
81 this._scheduledResourceRequests = {};
82 this._timerRecords = {};
84 this._calculator = new WebInspector.TimelineCalculator();
85 this._calculator._showShortEvents = false;
86 var shortRecordThresholdTitle = Number.secondsToString(WebInspector.TimelinePanel.shortRecordThreshold);
87 this._showShortRecordsTitleText = WebInspector.UIString("Show the records that are shorter than %s", shortRecordThresholdTitle);
88 this._hideShortRecordsTitleText = WebInspector.UIString("Hide the records that are shorter than %s", shortRecordThresholdTitle);
89 this._createStatusbarButtons();
91 this._boundariesAreValid = true;
94 this._popoverHelper = new WebInspector.PopoverHelper(this._containerElement, this._getPopoverAnchor.bind(this), this._showPopover.bind(this), true);
96 // Disable short events filter by default.
97 this.toggleFilterButton.toggled = true;
98 this._calculator._showShortEvents = this.toggleFilterButton.toggled;
99 this._markTimelineRecords = [];
100 this._expandOffset = 15;
102 InspectorBackend.registerDomainDispatcher("Timeline", this);
105 // Define row height, should be in sync with styles for timeline graphs.
106 WebInspector.TimelinePanel.rowHeight = 18;
107 WebInspector.TimelinePanel.shortRecordThreshold = 0.015;
109 WebInspector.TimelinePanel.prototype = {
110 _createTopPane: function() {
111 var topPaneElement = document.createElement("div");
112 topPaneElement.id = "timeline-overview-panel";
114 this._topPaneSidebarElement = document.createElement("div");
115 this._topPaneSidebarElement.id = "timeline-overview-sidebar";
117 var overviewTreeElement = document.createElement("ol");
118 overviewTreeElement.className = "sidebar-tree";
119 this._topPaneSidebarElement.appendChild(overviewTreeElement);
120 topPaneElement.appendChild(this._topPaneSidebarElement);
122 var topPaneSidebarTree = new TreeOutline(overviewTreeElement);
123 var timelinesOverviewItem = new WebInspector.SidebarTreeElement("resources-time-graph-sidebar-item", WebInspector.UIString("Timelines"));
124 topPaneSidebarTree.appendChild(timelinesOverviewItem);
125 timelinesOverviewItem.onselect = this._timelinesOverviewItemSelected.bind(this);
126 timelinesOverviewItem.select(true);
128 var memoryOverviewItem = new WebInspector.SidebarTreeElement("resources-size-graph-sidebar-item", WebInspector.UIString("Memory"));
129 topPaneSidebarTree.appendChild(memoryOverviewItem);
130 memoryOverviewItem.onselect = this._memoryOverviewItemSelected.bind(this);
132 this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories);
133 this._overviewPane.addEventListener("window changed", this._windowChanged, this);
134 this._overviewPane.addEventListener("filter changed", this._refresh, this);
135 topPaneElement.appendChild(this._overviewPane.element);
137 var separatorElement = document.createElement("div");
138 separatorElement.id = "timeline-overview-separator";
139 topPaneElement.appendChild(separatorElement);
140 return topPaneElement;
143 get toolbarItemLabel()
145 return WebInspector.UIString("Timeline");
150 return [this.toggleFilterButton.element, this.toggleTimelineButton.element, this.clearButton.element, this._overviewPane.statusBarFilters];
155 if (!this._categories) {
157 loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"),
158 scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"),
159 rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)")
162 return this._categories;
165 get defaultFocusedElement()
172 if (!this._recordStylesArray) {
173 var recordTypes = WebInspector.TimelineAgent.RecordType;
174 var recordStyles = {};
175 recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting };
176 recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering };
177 recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering };
178 recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering };
179 recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading };
180 recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting };
181 recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting };
182 recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting };
183 recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting };
184 recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting };
185 recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting };
186 recordStyles[recordTypes.MarkTimeline] = { title: WebInspector.UIString("Mark"), category: this.categories.scripting };
187 recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading };
188 recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading };
189 recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading };
190 recordStyles[recordTypes.FunctionCall] = { title: WebInspector.UIString("Function Call"), category: this.categories.scripting };
191 recordStyles[recordTypes.ResourceReceiveData] = { title: WebInspector.UIString("Receive Data"), category: this.categories.loading };
192 recordStyles[recordTypes.GCEvent] = { title: WebInspector.UIString("GC Event"), category: this.categories.scripting };
193 recordStyles[recordTypes.MarkDOMContentEventType] = { title: WebInspector.UIString("DOMContent event"), category: this.categories.scripting };
194 recordStyles[recordTypes.MarkLoadEventType] = { title: WebInspector.UIString("Load event"), category: this.categories.scripting };
195 recordStyles[recordTypes.ScheduleResourceRequest] = { title: WebInspector.UIString("Schedule Request"), category: this.categories.loading };
196 this._recordStylesArray = recordStyles;
198 return this._recordStylesArray;
201 _createStatusbarButtons: function()
203 this.toggleTimelineButton = new WebInspector.StatusBarButton(WebInspector.UIString("Record"), "record-profile-status-bar-item");
204 this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false);
206 this.clearButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear"), "clear-status-bar-item");
207 this.clearButton.addEventListener("click", this._clearPanel.bind(this), false);
209 this.toggleFilterButton = new WebInspector.StatusBarButton(this._hideShortRecordsTitleText, "timeline-filter-status-bar-item");
210 this.toggleFilterButton.addEventListener("click", this._toggleFilterButtonClicked.bind(this), false);
212 this.recordsCounter = document.createElement("span");
213 this.recordsCounter.className = "timeline-records-counter";
216 _updateRecordsCounter: function()
218 this.recordsCounter.textContent = WebInspector.UIString("%d of %d captured records are visible", this._rootRecord._visibleRecordsCount, this._rootRecord._allRecordsCount);
221 _updateEventDividers: function()
223 this._timelineGrid.removeEventDividers();
224 var clientWidth = this._graphRowsElement.offsetWidth - this._expandOffset;
226 for (var i = 0; i < this._markTimelineRecords.length; ++i) {
227 var record = this._markTimelineRecords[i];
228 var positions = this._calculator.computeBarGraphWindowPosition(record, clientWidth);
229 var dividerPosition = Math.round(positions.left);
230 if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition])
232 var divider = this._createEventDivider(record);
233 divider.style.left = (dividerPosition + this._expandOffset) + "px";
234 dividers[dividerPosition] = divider;
236 this._timelineGrid.addEventDividers(dividers);
237 this._overviewPane.updateEventDividers(this._markTimelineRecords, this._createEventDivider.bind(this));
240 _createEventDivider: function(record)
242 var eventDivider = document.createElement("div");
243 eventDivider.className = "resources-event-divider";
244 var recordTypes = WebInspector.TimelineAgent.RecordType;
246 var eventDividerPadding = document.createElement("div");
247 eventDividerPadding.className = "resources-event-divider-padding";
248 eventDividerPadding.title = record.title;
250 if (record.type === recordTypes.MarkDOMContentEventType)
251 eventDivider.className += " resources-blue-divider";
252 else if (record.type === recordTypes.MarkLoadEventType)
253 eventDivider.className += " resources-red-divider";
254 else if (record.type === recordTypes.MarkTimeline) {
255 eventDivider.className += " resources-orange-divider";
256 eventDividerPadding.title = record.data.message;
258 eventDividerPadding.appendChild(eventDivider);
259 return eventDividerPadding;
262 _timelinesOverviewItemSelected: function(event) {
263 this._overviewPane.showTimelines();
266 _memoryOverviewItemSelected: function(event) {
267 this._overviewPane.showMemoryGraph(this._rootRecord.children);
270 _toggleTimelineButtonClicked: function()
272 if (this.toggleTimelineButton.toggled)
273 InspectorBackend.stopTimelineProfiler();
276 InspectorBackend.startTimelineProfiler();
280 _toggleFilterButtonClicked: function()
282 this.toggleFilterButton.toggled = !this.toggleFilterButton.toggled;
283 this._calculator._showShortEvents = this.toggleFilterButton.toggled;
284 this.toggleFilterButton.element.title = this._calculator._showShortEvents ? this._hideShortRecordsTitleText : this._showShortRecordsTitleText;
285 this._scheduleRefresh(true);
288 timelineProfilerWasStarted: function()
290 this.toggleTimelineButton.toggled = true;
293 timelineProfilerWasStopped: function()
295 this.toggleTimelineButton.toggled = false;
298 addRecordToTimeline: function(record)
300 if (record.type == WebInspector.TimelineAgent.RecordType.ResourceSendRequest) {
301 var isMainResource = (record.data.identifier === WebInspector.mainResource.identifier);
302 if (isMainResource && this._mainResourceIdentifier !== record.data.identifier) {
303 // We are loading new main resource -> clear the panel. Check above is necessary since
304 // there may be several resource loads with main resource marker upon redirects, redirects are reported with
305 // the original identifier.
306 this._mainResourceIdentifier = record.data.identifier;
310 this._innerAddRecordToTimeline(record, this._rootRecord);
311 this._scheduleRefresh();
314 _findParentRecord: function(record)
316 var recordTypes = WebInspector.TimelineAgent.RecordType;
318 if (record.type === recordTypes.ResourceReceiveResponse ||
319 record.type === recordTypes.ResourceFinish ||
320 record.type === recordTypes.ResourceReceiveData)
321 parentRecord = this._sendRequestRecords[record.data.identifier];
322 else if (record.type === recordTypes.TimerFire)
323 parentRecord = this._timerRecords[record.data.timerId];
324 else if (record.type === recordTypes.ResourceSendRequest)
325 parentRecord = this._scheduledResourceRequests[record.data.url];
329 _innerAddRecordToTimeline: function(record, parentRecord)
331 var connectedToOldRecord = false;
332 var recordTypes = WebInspector.TimelineAgent.RecordType;
333 if (record.type === recordTypes.MarkDOMContentEventType || record.type === recordTypes.MarkLoadEventType)
334 parentRecord = null; // No bar entry for load events.
335 else if (parentRecord === this._rootRecord) {
336 var newParentRecord = this._findParentRecord(record);
337 if (newParentRecord) {
338 parentRecord = newParentRecord;
339 connectedToOldRecord = true;
343 if (record.type == recordTypes.TimerFire && record.children && record.children.length) {
344 var childRecord = record.children[0];
345 if (childRecord.type === recordTypes.FunctionCall) {
346 record.data.scriptName = childRecord.data.scriptName;
347 record.data.scriptLine = childRecord.data.scriptLine;
348 record.children.shift();
349 record.children = childRecord.children.concat(record.children);
353 var formattedRecord = new WebInspector.TimelinePanel.FormattedRecord(record, parentRecord, this);
355 if (record.type === recordTypes.MarkDOMContentEventType || record.type === recordTypes.MarkLoadEventType) {
356 this._markTimelineRecords.push(formattedRecord);
360 ++this._rootRecord._allRecordsCount;
361 formattedRecord.collapsed = (parentRecord === this._rootRecord);
363 var childrenCount = record.children ? record.children.length : 0;
364 for (var i = 0; i < childrenCount; ++i)
365 this._innerAddRecordToTimeline(record.children[i], formattedRecord);
367 formattedRecord._calculateAggregatedStats(this.categories);
369 if (connectedToOldRecord) {
370 var record = formattedRecord;
372 var parent = record.parent;
373 parent._cpuTime += formattedRecord._cpuTime;
374 if (parent._lastChildEndTime < record._lastChildEndTime)
375 parent._lastChildEndTime = record._lastChildEndTime;
376 for (var category in formattedRecord._aggregatedStats)
377 parent._aggregatedStats[category] += formattedRecord._aggregatedStats[category];
379 } while (record.parent);
381 if (parentRecord !== this._rootRecord)
382 parentRecord._selfTime -= formattedRecord.endTime - formattedRecord.startTime;
384 // Keep bar entry for mark timeline since nesting might be interesting to the user.
385 if (record.type === recordTypes.MarkTimeline)
386 this._markTimelineRecords.push(formattedRecord);
389 setSidebarWidth: function(width)
391 WebInspector.Panel.prototype.setSidebarWidth.call(this, width);
392 this._sidebarBackgroundElement.style.width = width + "px";
393 this._topPaneSidebarElement.style.width = width + "px";
396 updateMainViewWidth: function(width)
398 this._containerContentElement.style.left = width + "px";
399 this._scheduleRefresh();
400 this._overviewPane.updateMainViewWidth(width);
405 this._closeRecordDetails();
406 this._scheduleRefresh();
409 _createRootRecord: function()
412 rootRecord.children = [];
413 rootRecord._visibleRecordsCount = 0;
414 rootRecord._allRecordsCount = 0;
415 rootRecord._aggregatedStats = {};
419 _clearPanel: function()
421 this._markTimelineRecords = [];
422 this._sendRequestRecords = {};
423 this._scheduledResourceRequests = {};
424 this._timerRecords = {};
425 this._rootRecord = this._createRootRecord();
426 this._boundariesAreValid = false;
427 this._overviewPane.reset();
428 this._adjustScrollPosition(0);
430 this._closeRecordDetails();
435 WebInspector.Panel.prototype.show.call(this);
436 if (typeof this._scrollTop === "number")
437 this._containerElement.scrollTop = this._scrollTop;
439 WebInspector.drawer.currentPanelCounters = this.recordsCounter;
444 WebInspector.Panel.prototype.hide.call(this);
445 this._closeRecordDetails();
446 WebInspector.drawer.currentPanelCounters = null;
449 _onScroll: function(event)
451 this._closeRecordDetails();
452 var scrollTop = this._containerElement.scrollTop;
453 var dividersTop = Math.max(0, scrollTop);
454 this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
455 this._scheduleRefresh(true);
458 _windowChanged: function()
460 this._closeRecordDetails();
461 this._scheduleRefresh();
464 _scheduleRefresh: function(preserveBoundaries)
466 this._closeRecordDetails();
467 this._boundariesAreValid &= preserveBoundaries;
472 if (preserveBoundaries)
475 if (!this._refreshTimeout)
476 this._refreshTimeout = setTimeout(this._refresh.bind(this), 100);
481 if (this._refreshTimeout) {
482 clearTimeout(this._refreshTimeout);
483 delete this._refreshTimeout;
486 this._overviewPane.update(this._rootRecord.children, this._calculator._showShortEvents);
487 this._refreshRecords(!this._boundariesAreValid);
488 this._updateRecordsCounter();
489 if(!this._boundariesAreValid)
490 this._updateEventDividers();
491 this._boundariesAreValid = true;
494 _updateBoundaries: function()
496 this._calculator.reset();
497 this._calculator.windowLeft = this._overviewPane.windowLeft;
498 this._calculator.windowRight = this._overviewPane.windowRight;
500 for (var i = 0; i < this._rootRecord.children.length; ++i)
501 this._calculator.updateBoundaries(this._rootRecord.children[i]);
503 this._calculator.calculateWindow();
506 _addToRecordsWindow: function(record, recordsWindow, parentIsCollapsed)
508 if (!this._calculator._showShortEvents && !record.isLong())
510 var percentages = this._calculator.computeBarGraphPercentages(record);
511 if (percentages.start < 100 && percentages.endWithChildren >= 0 && !record.category.hidden) {
512 ++this._rootRecord._visibleRecordsCount;
513 ++record.parent._invisibleChildrenCount;
514 if (!parentIsCollapsed)
515 recordsWindow.push(record);
518 var index = recordsWindow.length;
519 record._invisibleChildrenCount = 0;
520 for (var i = 0; i < record.children.length; ++i)
521 this._addToRecordsWindow(record.children[i], recordsWindow, parentIsCollapsed || record.collapsed);
522 record._visibleChildrenCount = recordsWindow.length - index;
525 _filterRecords: function()
527 var recordsInWindow = [];
528 this._rootRecord._visibleRecordsCount = 0;
529 for (var i = 0; i < this._rootRecord.children.length; ++i)
530 this._addToRecordsWindow(this._rootRecord.children[i], recordsInWindow);
531 return recordsInWindow;
534 _refreshRecords: function(updateBoundaries)
536 if (updateBoundaries)
537 this._updateBoundaries();
539 var recordsInWindow = this._filterRecords();
541 // Calculate the visible area.
542 this._scrollTop = this._containerElement.scrollTop;
543 var visibleTop = this._scrollTop;
544 var visibleBottom = visibleTop + this._containerElement.clientHeight;
546 const rowHeight = WebInspector.TimelinePanel.rowHeight;
548 // Convert visible area to visible indexes. Always include top-level record for a visible nested record.
549 var startIndex = Math.max(0, Math.min(Math.floor(visibleTop / rowHeight) - 1, recordsInWindow.length - 1));
550 var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
552 // Resize gaps first.
553 const top = (startIndex * rowHeight) + "px";
554 this._topGapElement.style.height = top;
555 this.sidebarElement.style.top = top;
556 this.sidebarResizeElement.style.top = top;
557 this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
559 // Update visible rows.
560 var listRowElement = this._sidebarListElement.firstChild;
561 var width = this._graphRowsElement.offsetWidth;
562 this._itemsGraphsElement.removeChild(this._graphRowsElement);
563 var graphRowElement = this._graphRowsElement.firstChild;
564 var scheduleRefreshCallback = this._scheduleRefresh.bind(this, true);
565 this._itemsGraphsElement.removeChild(this._expandElements);
566 this._expandElements.removeChildren();
568 for (var i = 0; i < endIndex; ++i) {
569 var record = recordsInWindow[i];
570 var isEven = !(i % 2);
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 expandElement._update(record, i, this._calculator.computeBarGraphWindowPosition(record, width - this._expandOffset));
579 if (!listRowElement) {
580 listRowElement = new WebInspector.TimelineRecordListRow().element;
581 this._sidebarListElement.appendChild(listRowElement);
583 if (!graphRowElement) {
584 graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, scheduleRefreshCallback, rowHeight).element;
585 this._graphRowsElement.appendChild(graphRowElement);
588 listRowElement.row.update(record, isEven, this._calculator, visibleTop);
589 graphRowElement.row.update(record, isEven, this._calculator, width, this._expandOffset, i);
591 listRowElement = listRowElement.nextSibling;
592 graphRowElement = graphRowElement.nextSibling;
596 // Remove extra rows.
597 while (listRowElement) {
598 var nextElement = listRowElement.nextSibling;
599 listRowElement.row.dispose();
600 listRowElement = nextElement;
602 while (graphRowElement) {
603 var nextElement = graphRowElement.nextSibling;
604 graphRowElement.row.dispose();
605 graphRowElement = nextElement;
608 this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
609 this._itemsGraphsElement.appendChild(this._expandElements);
610 this.sidebarResizeElement.style.height = this.sidebarElement.clientHeight + "px";
611 // Reserve some room for expand / collapse controls to the left for records that start at 0ms.
612 var timelinePaddingLeft = this._calculator.windowLeft === 0 ? this._expandOffset : 0;
613 if (updateBoundaries)
614 this._timelineGrid.updateDividers(true, this._calculator, timelinePaddingLeft);
615 this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight);
618 _adjustScrollPosition: function(totalHeight)
620 // Prevent the container from being scrolled off the end.
621 if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1)
622 this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
625 _getPopoverAnchor: function(element)
627 return element.enclosingNodeOrSelfWithClass("timeline-graph-bar") || element.enclosingNodeOrSelfWithClass("timeline-tree-item");
630 _showPopover: function(anchor)
632 var record = anchor.row._record;
633 var popover = new WebInspector.Popover(record._generatePopupContent(this._calculator, this.categories));
634 popover.show(anchor);
638 _closeRecordDetails: function()
640 this._popoverHelper.hidePopup();
644 WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
646 WebInspector.TimelineCategory = function(name, title, color)
653 WebInspector.TimelineCalculator = function()
656 this.windowLeft = 0.0;
657 this.windowRight = 1.0;
660 WebInspector.TimelineCalculator.prototype = {
661 computeBarGraphPercentages: function(record)
663 var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
664 var end = (record.startTime + record._selfTime - this.minimumBoundary) / this.boundarySpan * 100;
665 var endWithChildren = (record._lastChildEndTime - this.minimumBoundary) / this.boundarySpan * 100;
666 var cpuWidth = record._cpuTime / this.boundarySpan * 100;
667 return {start: start, end: end, endWithChildren: endWithChildren, cpuWidth: cpuWidth};
670 computeBarGraphWindowPosition: function(record, clientWidth)
673 const borderWidth = 4;
674 var workingArea = clientWidth - minWidth - borderWidth;
675 var percentages = this.computeBarGraphPercentages(record);
676 var left = percentages.start / 100 * workingArea;
677 var width = (percentages.end - percentages.start) / 100 * workingArea + minWidth;
678 var widthWithChildren = (percentages.endWithChildren - percentages.start) / 100 * workingArea;
679 var cpuWidth = percentages.cpuWidth / 100 * workingArea + minWidth;
680 if (percentages.endWithChildren > percentages.end)
681 widthWithChildren += borderWidth + minWidth;
682 return {left: left, width: width, widthWithChildren: widthWithChildren, cpuWidth: cpuWidth};
685 calculateWindow: function()
687 this.minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
688 this.maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
689 this.boundarySpan = this.maximumBoundary - this.minimumBoundary;
694 this._absoluteMinimumBoundary = -1;
695 this._absoluteMaximumBoundary = -1;
698 updateBoundaries: function(record)
700 var lowerBound = record.startTime;
701 if (this._absoluteMinimumBoundary === -1 || lowerBound < this._absoluteMinimumBoundary)
702 this._absoluteMinimumBoundary = lowerBound;
704 const minimumTimeFrame = 0.1;
705 const minimumDeltaForZeroSizeEvents = 0.01;
706 var upperBound = Math.max(record._lastChildEndTime + minimumDeltaForZeroSizeEvents, lowerBound + minimumTimeFrame);
707 if (this._absoluteMaximumBoundary === -1 || upperBound > this._absoluteMaximumBoundary)
708 this._absoluteMaximumBoundary = upperBound;
711 formatValue: function(value)
713 return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary);
718 WebInspector.TimelineRecordListRow = function()
720 this.element = document.createElement("div");
721 this.element.row = this;
722 this.element.style.cursor = "pointer";
723 var iconElement = document.createElement("span");
724 iconElement.className = "timeline-tree-icon";
725 this.element.appendChild(iconElement);
727 this._typeElement = document.createElement("span");
728 this._typeElement.className = "type";
729 this.element.appendChild(this._typeElement);
731 var separatorElement = document.createElement("span");
732 separatorElement.className = "separator";
733 separatorElement.textContent = " ";
735 this._dataElement = document.createElement("span");
736 this._dataElement.className = "data dimmed";
738 this.element.appendChild(separatorElement);
739 this.element.appendChild(this._dataElement);
742 WebInspector.TimelineRecordListRow.prototype = {
743 update: function(record, isEven, calculator, offset)
745 this._record = record;
746 this._calculator = calculator;
747 this._offset = offset;
749 this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : "");
750 this._typeElement.textContent = record.title;
752 if (this._dataElement.firstChild)
753 this._dataElement.removeChildren();
754 if (record.details) {
755 var detailsContainer = document.createElement("span");
756 if (typeof record.details === "object") {
757 detailsContainer.appendChild(document.createTextNode("("));
758 detailsContainer.appendChild(record.details);
759 detailsContainer.appendChild(document.createTextNode(")"));
761 detailsContainer.textContent = "(" + record.details + ")";
762 this._dataElement.appendChild(detailsContainer);
768 this.element.parentElement.removeChild(this.element);
772 WebInspector.TimelineRecordGraphRow = function(graphContainer, scheduleRefresh)
774 this.element = document.createElement("div");
775 this.element.row = this;
777 this._barAreaElement = document.createElement("div");
778 this._barAreaElement.className = "timeline-graph-bar-area";
779 this.element.appendChild(this._barAreaElement);
781 this._barWithChildrenElement = document.createElement("div");
782 this._barWithChildrenElement.className = "timeline-graph-bar with-children";
783 this._barWithChildrenElement.row = this;
784 this._barAreaElement.appendChild(this._barWithChildrenElement);
786 this._barCpuElement = document.createElement("div");
787 this._barCpuElement.className = "timeline-graph-bar cpu"
788 this._barCpuElement.row = this;
789 this._barAreaElement.appendChild(this._barCpuElement);
791 this._barElement = document.createElement("div");
792 this._barElement.className = "timeline-graph-bar";
793 this._barElement.row = this;
794 this._barAreaElement.appendChild(this._barElement);
796 this._expandElement = new WebInspector.TimelineExpandableElement(graphContainer);
797 this._expandElement._element.addEventListener("click", this._onClick.bind(this));
799 this._scheduleRefresh = scheduleRefresh;
802 WebInspector.TimelineRecordGraphRow.prototype = {
803 update: function(record, isEven, calculator, clientWidth, expandOffset, index)
805 this._record = record;
806 this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : "");
807 var barPosition = calculator.computeBarGraphWindowPosition(record, clientWidth - expandOffset);
808 this._barWithChildrenElement.style.left = barPosition.left + expandOffset + "px";
809 this._barWithChildrenElement.style.width = barPosition.widthWithChildren + "px";
810 this._barElement.style.left = barPosition.left + expandOffset + "px";
811 this._barElement.style.width = barPosition.width + "px";
812 this._barCpuElement.style.left = barPosition.left + expandOffset + "px";
813 this._barCpuElement.style.width = barPosition.cpuWidth + "px";
814 this._expandElement._update(record, index, barPosition);
817 _onClick: function(event)
819 this._record.collapsed = !this._record.collapsed;
820 this._scheduleRefresh();
825 this.element.parentElement.removeChild(this.element);
826 this._expandElement._dispose();
830 WebInspector.TimelinePanel.FormattedRecord = function(record, parentRecord, panel)
832 var recordTypes = WebInspector.TimelineAgent.RecordType;
833 var style = panel._recordStyles[record.type];
835 this.parent = parentRecord;
837 parentRecord.children.push(this);
838 this.category = style.category;
839 this.title = style.title;
840 this.startTime = record.startTime / 1000;
841 this.data = record.data;
842 this.type = record.type;
843 this.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : this.startTime;
844 this._selfTime = this.endTime - this.startTime;
845 this._lastChildEndTime = this.endTime;
846 this.originalRecordForTests = record;
847 if (record.stackTrace && record.stackTrace.length)
848 this.stackTrace = record.stackTrace;
849 this.totalHeapSize = record.totalHeapSize;
850 this.usedHeapSize = record.usedHeapSize;
852 // Make resource receive record last since request was sent; make finish record last since response received.
853 if (record.type === recordTypes.ResourceSendRequest) {
854 panel._sendRequestRecords[record.data.identifier] = this;
855 } else if (record.type === recordTypes.ScheduleResourceRequest) {
856 panel._scheduledResourceRequests[record.data.url] = this;
857 } else if (record.type === recordTypes.ResourceReceiveResponse) {
858 var sendRequestRecord = panel._sendRequestRecords[record.data.identifier];
859 if (sendRequestRecord) { // False if we started instrumentation in the middle of request.
860 record.data.url = sendRequestRecord.data.url;
861 // Now that we have resource in the collection, recalculate details in order to display short url.
862 sendRequestRecord.details = this._getRecordDetails(sendRequestRecord, panel._sendRequestRecords);
863 if (sendRequestRecord.parent !== panel._rootRecord && sendRequestRecord.parent.type === recordTypes.ScheduleResourceRequest)
864 sendRequestRecord.parent.details = this._getRecordDetails(sendRequestRecord, panel._sendRequestRecords);
866 } else if (record.type === recordTypes.ResourceReceiveData) {
867 var sendRequestRecord = panel._sendRequestRecords[record.data.identifier];
868 if (sendRequestRecord) // False for main resource.
869 record.data.url = sendRequestRecord.data.url;
870 } else if (record.type === recordTypes.ResourceFinish) {
871 var sendRequestRecord = panel._sendRequestRecords[record.data.identifier];
872 if (sendRequestRecord) // False for main resource.
873 record.data.url = sendRequestRecord.data.url;
874 } else if (record.type === recordTypes.TimerInstall) {
875 this.timeout = record.data.timeout;
876 this.singleShot = record.data.singleShot;
877 panel._timerRecords[record.data.timerId] = this;
878 } else if (record.type === recordTypes.TimerFire) {
879 var timerInstalledRecord = panel._timerRecords[record.data.timerId];
880 if (timerInstalledRecord) {
881 this.callSiteStackTrace = timerInstalledRecord.stackTrace;
882 this.timeout = timerInstalledRecord.timeout;
883 this.singleShot = timerInstalledRecord.singleShot;
886 this.details = this._getRecordDetails(record, panel._sendRequestRecords);
889 WebInspector.TimelinePanel.FormattedRecord.prototype = {
892 return (this._lastChildEndTime - this.startTime) > WebInspector.TimelinePanel.shortRecordThreshold;
899 return this._children;
902 _generateAggregatedInfo: function()
904 var cell = document.createElement("span");
905 cell.className = "timeline-aggregated-info";
906 for (var index in this._aggregatedStats) {
907 var label = document.createElement("div");
908 label.className = "timeline-aggregated-category timeline-" + index;
909 cell.appendChild(label);
910 var text = document.createElement("span");
911 text.textContent = Number.secondsToString(this._aggregatedStats[index] + 0.0001);
912 cell.appendChild(text);
917 _generatePopupContent: function(calculator, categories)
919 var contentHelper = new WebInspector.TimelinePanel.PopupContentHelper(this.title);
921 if (this._children && this._children.length) {
922 contentHelper._appendTextRow(WebInspector.UIString("Self Time"), Number.secondsToString(this._selfTime + 0.0001));
923 contentHelper._appendElementRow(WebInspector.UIString("Aggregated Time"), this._generateAggregatedInfo());
925 var text = WebInspector.UIString("%s (at %s)", Number.secondsToString(this._lastChildEndTime - this.startTime),
926 calculator.formatValue(this.startTime - calculator.minimumBoundary));
927 contentHelper._appendTextRow(WebInspector.UIString("Duration"), text);
929 const recordTypes = WebInspector.TimelineAgent.RecordType;
932 case recordTypes.GCEvent:
933 contentHelper._appendTextRow(WebInspector.UIString("Collected"), Number.bytesToString(this.data.usedHeapSizeDelta));
935 case recordTypes.TimerInstall:
936 case recordTypes.TimerFire:
937 case recordTypes.TimerRemove:
938 contentHelper._appendTextRow(WebInspector.UIString("Timer ID"), this.data.timerId);
939 if (typeof this.timeout === "number") {
940 contentHelper._appendTextRow(WebInspector.UIString("Timeout"), Number.secondsToString(this.timeout / 1000));
941 contentHelper._appendTextRow(WebInspector.UIString("Repeats"), !this.singleShot);
944 case recordTypes.FunctionCall:
945 contentHelper._appendLinkRow(WebInspector.UIString("Location"), this.data.scriptName, this.data.scriptLine);
947 case recordTypes.ScheduleResourceRequest:
948 case recordTypes.ResourceSendRequest:
949 case recordTypes.ResourceReceiveResponse:
950 case recordTypes.ResourceReceiveData:
951 case recordTypes.ResourceFinish:
952 contentHelper._appendLinkRow(WebInspector.UIString("Resource"), this.data.url);
953 if (this.data.requestMethod)
954 contentHelper._appendTextRow(WebInspector.UIString("Request Method"), this.data.requestMethod);
955 if (typeof this.data.statusCode === "number")
956 contentHelper._appendTextRow(WebInspector.UIString("Status Code"), this.data.statusCode);
957 if (this.data.mimeType)
958 contentHelper._appendTextRow(WebInspector.UIString("MIME Type"), this.data.mimeType);
959 if (typeof this.data.expectedContentLength === "number" && this.data.expectedContentLength !== -1)
960 contentHelper._appendTextRow(WebInspector.UIString("Expected Content Length"), this.data.expectedContentLength);
962 case recordTypes.EvaluateScript:
963 if (this.data && this.data.url)
964 contentHelper._appendLinkRow(WebInspector.UIString("Script"), this.data.url, this.data.lineNumber);
966 case recordTypes.Paint:
967 contentHelper._appendTextRow(WebInspector.UIString("Location"), WebInspector.UIString("(%d, %d)", this.data.x, this.data.y));
968 contentHelper._appendTextRow(WebInspector.UIString("Dimensions"), WebInspector.UIString("%d × %d", this.data.width, this.data.height));
969 case recordTypes.RecalculateStyles: // We don't want to see default details.
973 contentHelper._appendTextRow(WebInspector.UIString("Details"), this.details);
977 if (this.data.scriptName && this.type !== recordTypes.FunctionCall)
978 contentHelper._appendLinkRow(WebInspector.UIString("Function Call"), this.data.scriptName, this.data.scriptLine);
980 if (this.usedHeapSize)
981 contentHelper._appendTextRow(WebInspector.UIString("Used Heap Size"), WebInspector.UIString("%s of %s", Number.bytesToString(this.usedHeapSize), Number.bytesToString(this.totalHeapSize)));
983 if (this.callSiteStackTrace && this.callSiteStackTrace.length)
984 contentHelper._appendStackTrace(WebInspector.UIString("Call Site stack"), this.callSiteStackTrace);
987 contentHelper._appendStackTrace(WebInspector.UIString("Call Stack"), this.stackTrace);
989 return contentHelper._contentTable;
992 _getRecordDetails: function(record, sendRequestRecords)
994 switch (record.type) {
995 case WebInspector.TimelineAgent.RecordType.GCEvent:
996 return WebInspector.UIString("%s collected", Number.bytesToString(record.data.usedHeapSizeDelta));
997 case WebInspector.TimelineAgent.RecordType.TimerFire:
998 return record.data.scriptName ? WebInspector.linkifyResourceAsNode(record.data.scriptName, "scripts", record.data.scriptLine, "", "") : record.data.timerId;
999 case WebInspector.TimelineAgent.RecordType.FunctionCall:
1000 return record.data.scriptName ? WebInspector.linkifyResourceAsNode(record.data.scriptName, "scripts", record.data.scriptLine, "", "") : null;
1001 case WebInspector.TimelineAgent.RecordType.EventDispatch:
1002 return record.data ? record.data.type : null;
1003 case WebInspector.TimelineAgent.RecordType.Paint:
1004 return record.data.width + "\u2009\u00d7\u2009" + record.data.height;
1005 case WebInspector.TimelineAgent.RecordType.TimerInstall:
1006 case WebInspector.TimelineAgent.RecordType.TimerRemove:
1007 return this.stackTrace ? WebInspector.linkifyResourceAsNode(this.stackTrace[0].scriptName, "scripts", this.stackTrace[0].lineNumber, "", "") : record.data.timerId;
1008 case WebInspector.TimelineAgent.RecordType.ParseHTML:
1009 case WebInspector.TimelineAgent.RecordType.RecalculateStyles:
1010 return this.stackTrace ? WebInspector.linkifyResourceAsNode(this.stackTrace[0].scriptName, "scripts", this.stackTrace[0].lineNumber, "", "") : null;
1011 case WebInspector.TimelineAgent.RecordType.EvaluateScript:
1012 return record.data.url ? WebInspector.linkifyResourceAsNode(record.data.url, "scripts", record.data.lineNumber, "", "") : null;
1013 case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange:
1014 case WebInspector.TimelineAgent.RecordType.XHRLoad:
1015 case WebInspector.TimelineAgent.RecordType.ScheduleResourceRequest:
1016 case WebInspector.TimelineAgent.RecordType.ResourceSendRequest:
1017 case WebInspector.TimelineAgent.RecordType.ResourceReceiveData:
1018 case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse:
1019 case WebInspector.TimelineAgent.RecordType.ResourceFinish:
1020 return WebInspector.displayNameForURL(record.data.url);
1021 case WebInspector.TimelineAgent.RecordType.MarkTimeline:
1022 return record.data.message;
1028 _calculateAggregatedStats: function(categories)
1030 this._aggregatedStats = {};
1031 for (var category in categories)
1032 this._aggregatedStats[category] = 0;
1033 this._cpuTime = this._selfTime;
1035 if (this._children) {
1036 for (var index = this._children.length; index; --index) {
1037 var child = this._children[index - 1];
1038 this._aggregatedStats[child.category.name] += child._selfTime;
1039 for (var category in categories)
1040 this._aggregatedStats[category] += child._aggregatedStats[category];
1042 for (var category in this._aggregatedStats)
1043 this._cpuTime += this._aggregatedStats[category];
1048 WebInspector.TimelinePanel.PopupContentHelper = function(title)
1050 this._contentTable = document.createElement("table");;
1051 var titleCell = this._createCell(WebInspector.UIString("%s - Details", title), "timeline-details-title");
1052 titleCell.colSpan = 2;
1053 var titleRow = document.createElement("tr");
1054 titleRow.appendChild(titleCell);
1055 this._contentTable.appendChild(titleRow);
1058 WebInspector.TimelinePanel.PopupContentHelper.prototype = {
1059 _createCell: function(content, styleName)
1061 var text = document.createElement("label");
1062 text.appendChild(document.createTextNode(content));
1063 var cell = document.createElement("td");
1064 cell.className = "timeline-details";
1066 cell.className += " " + styleName;
1067 cell.textContent = content;
1071 _appendTextRow: function(title, content)
1073 var row = document.createElement("tr");
1074 row.appendChild(this._createCell(title, "timeline-details-row-title"));
1075 row.appendChild(this._createCell(content, "timeline-details-row-data"));
1076 this._contentTable.appendChild(row);
1079 _appendElementRow: function(title, content, titleStyle)
1081 var row = document.createElement("tr");
1082 var titleCell = this._createCell(title, "timeline-details-row-title");
1084 titleCell.addStyleClass(titleStyle);
1085 row.appendChild(titleCell);
1086 var cell = document.createElement("td");
1087 cell.className = "timeline-details";
1088 cell.appendChild(content);
1089 row.appendChild(cell);
1090 this._contentTable.appendChild(row);
1093 _appendLinkRow: function(title, scriptName, scriptLine)
1095 var link = WebInspector.linkifyResourceAsNode(scriptName, "scripts", scriptLine, "timeline-details");
1096 this._appendElementRow(title, link);
1099 _appendStackTrace: function(title, stackTrace)
1101 this._appendTextRow("", "");
1102 var framesTable = document.createElement("table");
1103 for (var i = 0; i < stackTrace.length; ++i) {
1104 var stackFrame = stackTrace[i];
1105 var row = document.createElement("tr");
1106 row.className = "timeline-details";
1107 row.appendChild(this._createCell(stackFrame.functionName ? stackFrame.functionName : WebInspector.UIString("(anonymous function)"), "timeline-function-name"));
1108 row.appendChild(this._createCell(" @ "));
1109 var linkCell = document.createElement("td");
1110 linkCell.appendChild(WebInspector.linkifyResourceAsNode(stackFrame.scriptName, "scripts", stackFrame.lineNumber, "timeline-details"));
1111 row.appendChild(linkCell);
1112 framesTable.appendChild(row);
1114 this._appendElementRow(title, framesTable, "timeline-stacktrace-title");
1118 WebInspector.TimelineExpandableElement = function(container)
1120 this._element = document.createElement("div");
1121 this._element.className = "timeline-expandable";
1123 var leftBorder = document.createElement("div");
1124 leftBorder.className = "timeline-expandable-left";
1125 this._element.appendChild(leftBorder);
1127 container.appendChild(this._element);
1130 WebInspector.TimelineExpandableElement.prototype = {
1131 _update: function(record, index, barPosition)
1133 const rowHeight = WebInspector.TimelinePanel.rowHeight;
1134 if (record._visibleChildrenCount || record._invisibleChildrenCount) {
1135 this._element.style.top = index * rowHeight + "px";
1136 this._element.style.left = barPosition.left + "px";
1137 this._element.style.width = Math.max(12, barPosition.width + 25) + "px";
1138 if (!record.collapsed) {
1139 this._element.style.height = (record._visibleChildrenCount + 1) * rowHeight + "px";
1140 this._element.addStyleClass("timeline-expandable-expanded");
1141 this._element.removeStyleClass("timeline-expandable-collapsed");
1143 this._element.style.height = rowHeight + "px";
1144 this._element.addStyleClass("timeline-expandable-collapsed");
1145 this._element.removeStyleClass("timeline-expandable-expanded");
1147 this._element.removeStyleClass("hidden");
1149 this._element.addStyleClass("hidden");
1152 _dispose: function()
1154 this._element.parentElement.removeChild(this._element);