| /* |
| * 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; |
| } |
| |
| 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(); |
| } |
| }; |