WebInspector.TimelinePanel = function()
{
WebInspector.Panel.call(this);
this.element.addStyleClass("timeline");
this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories);
this._overviewPane.addEventListener("window changed", this._windowChanged, this);
this._overviewPane.addEventListener("filter changed", this._refresh, this);
this.element.appendChild(this._overviewPane.element);
this._sidebarBackgroundElement = document.createElement("div");
this._sidebarBackgroundElement.className = "sidebar timeline-sidebar-background";
this.element.appendChild(this._sidebarBackgroundElement);
this._containerElement = document.createElement("div");
this._containerElement.id = "timeline-container";
this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
this.element.appendChild(this._containerElement);
this.createSidebar(this._containerElement, this._containerElement);
var itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
itemsTreeElement.expanded = true;
this.sidebarTree.appendChild(itemsTreeElement);
this._sidebarListElement = document.createElement("div");
this.sidebarElement.appendChild(this._sidebarListElement);
this._containerContentElement = document.createElement("div");
this._containerContentElement.id = "resources-container-content";
this._containerElement.appendChild(this._containerContentElement);
this._timelineGrid = new WebInspector.TimelineGrid();
this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
this._itemsGraphsElement.id = "timeline-graphs";
this._containerContentElement.appendChild(this._timelineGrid.element);
this._topGapElement = document.createElement("div");
this._topGapElement.className = "timeline-gap";
this._itemsGraphsElement.appendChild(this._topGapElement);
this._graphRowsElement = document.createElement("div");
this._itemsGraphsElement.appendChild(this._graphRowsElement);
this._bottomGapElement = document.createElement("div");
this._bottomGapElement.className = "timeline-gap";
this._itemsGraphsElement.appendChild(this._bottomGapElement);
this._createStatusbarButtons();
this._records = [];
this._sendRequestRecords = {};
this._calculator = new WebInspector.TimelineCalculator();
this._boundariesAreValid = true;
}
WebInspector.TimelinePanel.prototype = {
toolbarItemClass: "timeline",
get toolbarItemLabel()
{
return WebInspector.UIString("Timeline");
},
get statusBarItems()
{
return [this.toggleTimelineButton.element, this.clearButton.element];
},
get categories()
{
if (!this._categories) {
this._categories = {
loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"),
scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"),
rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)")
};
}
return this._categories;
},
_createStatusbarButtons: function()
{
this.toggleTimelineButton = new WebInspector.StatusBarButton("", "record-profile-status-bar-item");
this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false);
this.clearButton = new WebInspector.StatusBarButton("", "timeline-clear-status-bar-item");
this.clearButton.addEventListener("click", this.reset.bind(this), false);
},
_toggleTimelineButtonClicked: function()
{
if (this.toggleTimelineButton.toggled)
InspectorBackend.stopTimelineProfiler();
else
InspectorBackend.startTimelineProfiler();
},
timelineWasStarted: function()
{
this.toggleTimelineButton.toggled = true;
},
timelineWasStopped: function()
{
this.toggleTimelineButton.toggled = false;
},
addRecordToTimeline: function(record)
{
this._innerAddRecordToTimeline(record, this._records);
this._scheduleRefresh();
},
_innerAddRecordToTimeline: function(record, collection)
{
var formattedRecord = this._formatRecord(record);
if (this._lastRecord && (!record.children || !record.children.length) &&
this._lastRecord.category == formattedRecord.category &&
this._lastRecord.title == formattedRecord.title &&
this._lastRecord.details == formattedRecord.details &&
formattedRecord.startTime - this._lastRecord.endTime < 0.1) {
this._lastRecord.endTime = formattedRecord.endTime;
this._lastRecord.count++;
} else {
collection.push(formattedRecord);
for (var i = 0; record.children && i < record.children.length; ++i) {
if (!formattedRecord.children)
formattedRecord.children = [];
var formattedChild = this._innerAddRecordToTimeline(record.children[i], formattedRecord.children);
formattedChild.parent = formattedRecord;
}
this._lastRecord = record.children && record.children.length ? null : formattedRecord;
}
return formattedRecord;
},
_formatRecord: function(record)
{
var recordTypes = WebInspector.TimelineAgent.RecordType;
if (!this._recordStyles) {
this._recordStyles = {};
this._recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting };
this._recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering };
this._recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering };
this._recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering };
this._recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading };
this._recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting };
this._recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting };
this._recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting };
this._recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting };
this._recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting };
this._recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting };
this._recordStyles[recordTypes.MarkTimeline] = { title: WebInspector.UIString("Mark"), category: this.categories.scripting };
this._recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading };
this._recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading };
this._recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading };
}
var style = this._recordStyles[record.type];
if (!style)
style = this._recordStyles[recordTypes.EventDispatch];
var formattedRecord = {};
formattedRecord.category = style.category;
formattedRecord.title = style.title;
formattedRecord.startTime = record.startTime / 1000;
formattedRecord.data = record.data;
formattedRecord.count = 1;
formattedRecord.type = record.type;
formattedRecord.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : formattedRecord.startTime;
formattedRecord.record = record;
if (record.type === WebInspector.TimelineAgent.RecordType.ResourceSendRequest) {
this._sendRequestRecords[record.data.identifier] = formattedRecord;
} else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse) {
var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
if (sendRequestRecord) { sendRequestRecord._responseReceivedFormattedTime = formattedRecord.startTime;
formattedRecord.startTime = sendRequestRecord.startTime;
sendRequestRecord.details = this._getRecordDetails(record);
}
} else if (record.type === WebInspector.TimelineAgent.RecordType.ResourceFinish) {
var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
if (sendRequestRecord) formattedRecord.startTime = sendRequestRecord._responseReceivedFormattedTime;
}
formattedRecord.details = this._getRecordDetails(record);
return formattedRecord;
},
_getRecordDetails: function(record)
{
switch (record.type) {
case WebInspector.TimelineAgent.RecordType.EventDispatch:
return record.data ? record.data.type : "";
case WebInspector.TimelineAgent.RecordType.Paint:
return record.data.width + "\u2009\u00d7\u2009" + record.data.height;
case WebInspector.TimelineAgent.RecordType.TimerInstall:
case WebInspector.TimelineAgent.RecordType.TimerRemove:
case WebInspector.TimelineAgent.RecordType.TimerFire:
return record.data.timerId;
case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange:
case WebInspector.TimelineAgent.RecordType.XHRLoad:
case WebInspector.TimelineAgent.RecordType.EvaluateScript:
case WebInspector.TimelineAgent.RecordType.ResourceSendRequest:
return WebInspector.displayNameForURL(record.data.url);
case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse:
case WebInspector.TimelineAgent.RecordType.ResourceFinish:
var sendRequestRecord = this._sendRequestRecords[record.data.identifier];
return sendRequestRecord ? WebInspector.displayNameForURL(sendRequestRecord.data.url) : "";
case WebInspector.TimelineAgent.RecordType.MarkTimeline:
return record.data.message;
default:
return "";
}
},
setSidebarWidth: function(width)
{
WebInspector.Panel.prototype.setSidebarWidth.call(this, width);
this._sidebarBackgroundElement.style.width = width + "px";
this._overviewPane.setSidebarWidth(width);
},
updateMainViewWidth: function(width)
{
this._containerContentElement.style.left = width + "px";
this._scheduleRefresh();
this._overviewPane.updateMainViewWidth(width);
},
resize: function() {
this._scheduleRefresh();
},
reset: function()
{
this._lastRecord = null;
this._sendRequestRecords = {};
this._records = [];
this._boundariesAreValid = false;
this._overviewPane.reset();
this._adjustScrollPosition(0);
this._refresh();
},
show: function()
{
WebInspector.Panel.prototype.show.call(this);
if (this._needsRefresh)
this._refresh();
},
_onScroll: function(event)
{
var scrollTop = this._containerElement.scrollTop;
var dividersTop = Math.max(0, scrollTop);
this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
this._scheduleRefresh(true);
},
_windowChanged: function()
{
this._scheduleRefresh();
},
_scheduleRefresh: function(preserveBoundaries)
{
this._boundariesAreValid &= preserveBoundaries;
if (this._needsRefresh)
return;
this._needsRefresh = true;
if (this.visible && !("_refreshTimeout" in this)) {
if (preserveBoundaries)
this._refresh();
else
this._refreshTimeout = setTimeout(this._refresh.bind(this), 100);
}
},
_refresh: function()
{
this._needsRefresh = false;
if ("_refreshTimeout" in this) {
clearTimeout(this._refreshTimeout);
delete this._refreshTimeout;
}
if (!this._boundariesAreValid)
this._overviewPane.update(this._records);
this._refreshRecords(!this._boundariesAreValid);
this._boundariesAreValid = true;
},
_refreshRecords: function(updateBoundaries)
{
if (updateBoundaries) {
this._calculator.reset();
this._calculator.windowLeft = this._overviewPane.windowLeft;
this._calculator.windowRight = this._overviewPane.windowRight;
for (var i = 0; i < this._records.length; ++i)
this._calculator.updateBoundaries(this._records[i]);
this._calculator.calculateWindow();
}
var recordsInWindow = [];
for (var i = 0; i < this._records.length; ++i) {
var record = this._records[i];
var percentages = this._calculator.computeBarGraphPercentages(record);
if (percentages.start < 100 && percentages.end >= 0 && !record.category.hidden)
this._addToRecordsWindow(record, recordsInWindow);
}
var visibleTop = this._containerElement.scrollTop;
var visibleBottom = visibleTop + this._containerElement.clientHeight;
const rowHeight = 18;
const expandOffset = 15;
var startIndex = Math.max(0, Math.min(Math.floor(visibleTop / rowHeight) - 1, recordsInWindow.length - 1));
while (startIndex > 0 && recordsInWindow[startIndex].parent)
startIndex--;
var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));
while (endIndex < recordsInWindow.length - 1 && recordsInWindow[endIndex].parent)
endIndex++;
const top = (startIndex * rowHeight) + "px";
this._topGapElement.style.height = top;
this.sidebarElement.style.top = top;
this.sidebarResizeElement.style.top = top;
this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";
var listRowElement = this._sidebarListElement.firstChild;
var width = this._graphRowsElement.offsetWidth;
this._itemsGraphsElement.removeChild(this._graphRowsElement);
var graphRowElement = this._graphRowsElement.firstChild;
var scheduleRefreshCallback = this._scheduleRefresh.bind(this, true);
for (var i = startIndex; i < endIndex; ++i) {
var record = recordsInWindow[i];
var isEven = !(i % 2);
if (!listRowElement) {
listRowElement = new WebInspector.TimelineRecordListRow().element;
this._sidebarListElement.appendChild(listRowElement);
}
if (!graphRowElement) {
graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, scheduleRefreshCallback, rowHeight).element;
this._graphRowsElement.appendChild(graphRowElement);
}
listRowElement.listRow.update(record, isEven);
graphRowElement.graphRow.update(record, isEven, this._calculator, width, expandOffset, i);
listRowElement = listRowElement.nextSibling;
graphRowElement = graphRowElement.nextSibling;
}
while (listRowElement) {
var nextElement = listRowElement.nextSibling;
listRowElement.listRow.dispose();
listRowElement = nextElement;
}
while (graphRowElement) {
var nextElement = graphRowElement.nextSibling;
graphRowElement.graphRow.dispose();
graphRowElement = nextElement;
}
this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
var timelinePaddingLeft = this._calculator.windowLeft === 0 ? expandOffset : 0;
if (updateBoundaries)
this._timelineGrid.updateDividers(true, this._calculator, timelinePaddingLeft);
this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight);
},
_addToRecordsWindow: function(record, recordsWindow)
{
recordsWindow.push(record);
if (!record.collapsed) {
var index = recordsWindow.length;
for (var i = 0; record.children && i < record.children.length; ++i)
this._addToRecordsWindow(record.children[i], recordsWindow);
record.visibleChildrenCount = recordsWindow.length - index;
}
},
_adjustScrollPosition: function(totalHeight)
{
if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1)
this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
}
}
WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
WebInspector.TimelineCategory = function(name, title, color)
{
this.name = name;
this.title = title;
this.color = color;
}
WebInspector.TimelineCalculator = function()
{
this.reset();
this.windowLeft = 0.0;
this.windowRight = 1.0;
this._uiString = WebInspector.UIString.bind(WebInspector);
}
WebInspector.TimelineCalculator.prototype = {
computeBarGraphPercentages: function(record)
{
var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
var end = (record.endTime - this.minimumBoundary) / this.boundarySpan * 100;
return {start: start, end: end};
},
calculateWindow: function()
{
this.minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
this.maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
this.boundarySpan = this.maximumBoundary - this.minimumBoundary;
},
reset: function()
{
this._absoluteMinimumBoundary = -1;
this._absoluteMaximumBoundary = -1;
},
updateBoundaries: function(record)
{
var lowerBound = record.startTime;
if (this._absoluteMinimumBoundary === -1 || lowerBound < this._absoluteMinimumBoundary)
this._absoluteMinimumBoundary = lowerBound;
var upperBound = record.endTime;
if (this._absoluteMaximumBoundary === -1 || upperBound > this._absoluteMaximumBoundary)
this._absoluteMaximumBoundary = upperBound;
},
formatValue: function(value)
{
return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary, this._uiString);
}
}
WebInspector.TimelineRecordListRow = function()
{
this.element = document.createElement("div");
this.element.listRow = this;
var iconElement = document.createElement("span");
iconElement.className = "timeline-tree-icon";
this.element.appendChild(iconElement);
this._typeElement = document.createElement("span");
this._typeElement.className = "type";
this.element.appendChild(this._typeElement);
var separatorElement = document.createElement("span");
separatorElement.className = "separator";
separatorElement.textContent = " ";
this._dataElement = document.createElement("span");
this._dataElement.className = "data dimmed";
this._repeatCountElement = document.createElement("span");
this._repeatCountElement.className = "count";
this.element.appendChild(separatorElement);
this.element.appendChild(this._dataElement);
this.element.appendChild(this._repeatCountElement);
}
WebInspector.TimelineRecordListRow.prototype = {
update: function(record, isEven)
{
this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : "");
this._typeElement.textContent = record.title;
if (record.details) {
this._dataElement.textContent = "(" + record.details + ")";
this._dataElement.title = record.details;
} else {
this._dataElement.textContent = "";
this._dataElement.title = "";
}
if (record.count > 1)
this._repeatCountElement.textContent = "\u2009\u00d7\u2009" + record.count;
else
this._repeatCountElement.textContent = "";
},
dispose: function()
{
this.element.parentElement.removeChild(this.element);
}
}
WebInspector.TimelineRecordGraphRow = function(graphContainer, refreshCallback, rowHeight)
{
this.element = document.createElement("div");
this.element.graphRow = this;
this._barAreaElement = document.createElement("div");
this._barAreaElement.className = "timeline-graph-bar-area";
this.element.appendChild(this._barAreaElement);
this._barElement = document.createElement("div");
this._barElement.className = "timeline-graph-bar";
this._barAreaElement.appendChild(this._barElement);
this._expandElement = document.createElement("div");
this._expandElement.className = "timeline-expandable";
graphContainer.appendChild(this._expandElement);
var leftBorder = document.createElement("div");
leftBorder.className = "timeline-expandable-left";
this._expandElement.appendChild(leftBorder);
this._expandElement.addEventListener("click", this._onClick.bind(this));
this._refreshCallback = refreshCallback;
this._rowHeight = rowHeight;
}
WebInspector.TimelineRecordGraphRow.prototype = {
update: function(record, isEven, calculator, clientWidth, expandOffset, index)
{
this._record = record;
this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : "");
var percentages = calculator.computeBarGraphPercentages(record);
var left = percentages.start / 100 * clientWidth;
var width = (percentages.end - percentages.start) / 100 * clientWidth;
this._barElement.style.left = (left + expandOffset) + "px";
this._barElement.style.width = width + "px";
if (record.visibleChildrenCount) {
this._expandElement.style.top = index * this._rowHeight + "px";
this._expandElement.style.left = left + "px";
this._expandElement.style.width = Math.max(12, width + 25) + "px";
if (!record.collapsed) {
this._expandElement.style.height = (record.visibleChildrenCount + 1) * this._rowHeight + "px";
this._expandElement.addStyleClass("timeline-expandable-expanded");
this._expandElement.removeStyleClass("timeline-expandable-collapsed");
} else {
this._expandElement.style.height = this._rowHeight + "px";
this._expandElement.addStyleClass("timeline-expandable-collapsed");
this._expandElement.removeStyleClass("timeline-expandable-expanded");
}
this._expandElement.removeStyleClass("hidden");
} else {
this._expandElement.addStyleClass("hidden");
}
},
_onClick: function(event)
{
this._record.collapsed = !this._record.collapsed;
this._refreshCallback();
},
dispose: function()
{
this.element.parentElement.removeChild(this.element);
this._expandElement.parentElement.removeChild(this._expandElement);
}
}