blob: 33076bfab933bb5bf7dbb01eb6a6c95e02f2025f [file] [log] [blame]
/*
* Copyright (C) 2014, 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
WI.TimelineDataGridNode = class TimelineDataGridNode extends WI.DataGridNode
{
constructor(records, options = {})
{
super({}, options.hasChildren);
this.copyable = false;
this._records = records;
this._includesGraph = options.includesGraph || false;
this._graphDataSource = options.graphDataSource || null;
this._cachedData = null;
if (this._graphDataSource) {
this._graphContainerElement = document.createElement("div");
this._timelineRecordBars = [];
}
}
// Public
get records() { return this._records; }
get record()
{
return this.records && this.records.length ? this.records[0] : null;
}
get graphDataSource()
{
return this._graphDataSource;
}
get data()
{
if (!this._graphDataSource)
return {};
return {
graph: this.record ? this.record.startTime : 0,
};
}
collapse()
{
super.collapse();
if (!this._graphDataSource || !this.revealed)
return;
// Refresh to show child bars in our graph now that we collapsed.
this.refreshGraph();
}
expand()
{
super.expand();
if (!this._graphDataSource || !this.revealed)
return;
// Refresh to remove child bars from our graph now that we expanded.
this.refreshGraph();
// Refresh child graphs since they haven't been updating while we were collapsed.
var childNode = this.children[0];
while (childNode) {
if (childNode instanceof WI.TimelineDataGridNode)
childNode.refreshGraph();
childNode = childNode.traverseNextNode(true, this);
}
}
createCellContent(columnIdentifier, cell)
{
if (columnIdentifier === "graph" && this._graphDataSource) {
this.needsGraphRefresh();
return this._graphContainerElement;
}
var value = this.data[columnIdentifier];
if (!value)
return emDash;
const options = {
useGoToArrowButton: true,
ignoreNetworkTab: true,
ignoreSearchTab: true,
};
if (value instanceof WI.SourceCodeLocation) {
if (value.sourceCode instanceof WI.Resource) {
cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, ...WI.Resource.classNamesForResource(value.sourceCode));
} else if (value.sourceCode instanceof WI.Script) {
if (value.sourceCode.url) {
cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName);
cell.classList.add(WI.Resource.Type.Script);
} else
cell.classList.add(WI.ScriptTreeElement.AnonymousScriptIconStyleClassName);
} else
console.error("Unknown SourceCode subclass.");
// Give the whole cell a tooltip and keep it up to date.
value.populateLiveDisplayLocationTooltip(cell);
var fragment = document.createDocumentFragment();
fragment.appendChild(WI.createSourceCodeLocationLink(value, options));
var titleElement = document.createElement("span");
value.populateLiveDisplayLocationString(titleElement, "textContent");
fragment.appendChild(titleElement);
return fragment;
}
if (value instanceof WI.CallFrame) {
var callFrame = value;
var isAnonymousFunction = false;
var functionName = callFrame.functionName;
if (!functionName) {
functionName = WI.UIString("(anonymous function)");
isAnonymousFunction = true;
}
cell.classList.add(WI.CallFrameView.FunctionIconStyleClassName);
var fragment = document.createDocumentFragment();
if (callFrame.sourceCodeLocation && callFrame.sourceCodeLocation.sourceCode) {
// Give the whole cell a tooltip and keep it up to date.
callFrame.sourceCodeLocation.populateLiveDisplayLocationTooltip(cell);
fragment.appendChild(WI.createSourceCodeLocationLink(callFrame.sourceCodeLocation, options));
if (isAnonymousFunction) {
// For anonymous functions we show the resource or script icon and name.
if (callFrame.sourceCodeLocation.sourceCode instanceof WI.Resource) {
cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName, ...WI.Resource.classNamesForResource(callFrame.sourceCodeLocation.sourceCode));
} else if (callFrame.sourceCodeLocation.sourceCode instanceof WI.Script) {
if (callFrame.sourceCodeLocation.sourceCode.url) {
cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName);
cell.classList.add(WI.Resource.Type.Script);
} else
cell.classList.add(WI.ScriptTreeElement.AnonymousScriptIconStyleClassName);
} else
console.error("Unknown SourceCode subclass.");
var titleElement = document.createElement("span");
callFrame.sourceCodeLocation.populateLiveDisplayLocationString(titleElement, "textContent");
fragment.appendChild(titleElement);
} else {
// Show the function name and icon.
cell.classList.add(WI.CallFrameView.FunctionIconStyleClassName);
fragment.append(functionName);
var subtitleElement = document.createElement("span");
subtitleElement.classList.add("subtitle");
callFrame.sourceCodeLocation.populateLiveDisplayLocationString(subtitleElement, "textContent");
fragment.appendChild(subtitleElement);
}
return fragment;
}
var icon = document.createElement("div");
icon.classList.add("icon");
fragment.append(icon, functionName);
return fragment;
}
if (value instanceof WI.DOMNode) {
cell.classList.add(WI.DOMTreeElementPathComponent.iconClassNameForNode(value));
return WI.linkifyNodeReference(value);
}
return super.createCellContent(columnIdentifier, cell);
}
refresh()
{
this._cachedData = null;
if (this._graphDataSource && this._includesGraph)
this.needsGraphRefresh();
super.refresh();
}
refreshGraph()
{
if (!this._graphDataSource)
return;
if (this._scheduledGraphRefreshIdentifier) {
cancelAnimationFrame(this._scheduledGraphRefreshIdentifier);
this._scheduledGraphRefreshIdentifier = undefined;
}
// We are not visible, but an ancestor will draw our graph.
// They need notified by using our needsGraphRefresh.
console.assert(this.revealed);
if (!this.revealed)
return;
let secondsPerPixel = this._graphDataSource.secondsPerPixel;
if (isNaN(secondsPerPixel))
return;
console.assert(secondsPerPixel > 0);
var recordBarIndex = 0;
function createBar(records, renderMode)
{
var timelineRecordBar = this._timelineRecordBars[recordBarIndex];
if (!timelineRecordBar)
timelineRecordBar = this._timelineRecordBars[recordBarIndex] = new WI.TimelineRecordBar(this, records, renderMode);
else {
timelineRecordBar.renderMode = renderMode;
timelineRecordBar.records = records;
}
timelineRecordBar.refresh(this._graphDataSource);
if (!timelineRecordBar.element.parentNode) {
this._graphContainerElement.appendChild(timelineRecordBar.element);
this.didAddRecordBar(timelineRecordBar);
}
++recordBarIndex;
}
function collectRecordsByType(records, recordsByTypeMap)
{
for (var record of records) {
var typedRecords = recordsByTypeMap.get(record.type);
if (!typedRecords) {
typedRecords = [];
recordsByTypeMap.set(record.type, typedRecords);
}
typedRecords.push(record);
}
}
var boundCreateBar = createBar.bind(this);
if (this.expanded) {
// When expanded just use the records for this node.
WI.TimelineRecordBar.createCombinedBars(this.records, secondsPerPixel, this._graphDataSource, boundCreateBar);
} else {
// When collapsed use the records for this node and its descendants.
// To share bars better, group records by type.
var recordTypeMap = new Map;
collectRecordsByType(this.records, recordTypeMap);
var childNode = this.children[0];
while (childNode) {
if (childNode instanceof WI.TimelineDataGridNode)
collectRecordsByType(childNode.records, recordTypeMap);
childNode = childNode.traverseNextNode(false, this);
}
for (var records of recordTypeMap.values())
WI.TimelineRecordBar.createCombinedBars(records, secondsPerPixel, this._graphDataSource, boundCreateBar);
}
// Remove the remaining unused TimelineRecordBars.
for (; recordBarIndex < this._timelineRecordBars.length; ++recordBarIndex) {
this._timelineRecordBars[recordBarIndex].element.remove();
this.didRemoveRecordBar(this._timelineRecordBars[recordBarIndex]);
this._timelineRecordBars[recordBarIndex].records = null;
}
}
needsGraphRefresh()
{
if (!this.revealed) {
// We are not visible, but an ancestor will be drawing our graph.
// Notify the next visible ancestor that their graph needs to refresh.
var ancestor = this;
while (ancestor && !ancestor.root) {
if (ancestor.revealed && ancestor instanceof WI.TimelineDataGridNode) {
ancestor.needsGraphRefresh();
return;
}
ancestor = ancestor.parent;
}
return;
}
if (!this._graphDataSource || this._scheduledGraphRefreshIdentifier)
return;
this._scheduledGraphRefreshIdentifier = requestAnimationFrame(this.refreshGraph.bind(this));
}
displayName()
{
// Can be overridden by subclasses.
const includeDetailsInMainTitle = true;
return WI.TimelineTabContentView.displayNameForRecord(this.record, includeDetailsInMainTitle);
}
iconClassNames()
{
// Can be overridden by subclasses.
return [WI.TimelineTabContentView.iconClassNameForRecord(this.record)];
}
// Protected
createGoToArrowButton(cellElement, callback)
{
function buttonClicked(event)
{
if (this.hidden || !this.revealed)
return;
event.stopPropagation();
callback(this, cellElement.__columnIdentifier);
}
let button = WI.createGoToArrowButton();
button.addEventListener("click", buttonClicked.bind(this));
let contentElement = cellElement.firstChild;
contentElement.appendChild(button);
}
isRecordVisible(record)
{
if (!this._graphDataSource)
return false;
if (isNaN(record.startTime))
return false;
// If this bar is completely before the bounds of the graph, not visible.
if (record.endTime < this.graphDataSource.startTime)
return false;
// If this record is completely after the current time or end time, not visible.
if (record.startTime > this.graphDataSource.currentTime || record.startTime > this.graphDataSource.endTime)
return false;
return true;
}
filterableDataForColumn(columnIdentifier)
{
let value = this.data[columnIdentifier];
if (value instanceof WI.SourceCodeLocation)
return value.displayLocationString();
if (value instanceof WI.CallFrame)
return [value.functionName, value.sourceCodeLocation.displayLocationString()];
return super.filterableDataForColumn(columnIdentifier);
}
didAddRecordBar(recordBar)
{
// Implemented by subclasses.
}
didRemoveRecordBar(recordBar)
{
// Implemented by subclasses.
}
didResizeColumn(columnIdentifier)
{
if (columnIdentifier !== "graph")
return;
this.needsGraphRefresh();
}
};