| // Copyright 2016 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| /** | |
| * @unrestricted | |
| */ | |
| export class Node { | |
| /** | |
| * @param {string|symbol} id | |
| * @param {?SDK.TracingModel.Event} event | |
| */ | |
| constructor(id, event) { | |
| /** @type {number} */ | |
| this.totalTime = 0; | |
| /** @type {number} */ | |
| this.selfTime = 0; | |
| /** @type {string|symbol} */ | |
| this.id = id; | |
| /** @type {?SDK.TracingModel.Event} */ | |
| this.event = event; | |
| /** @type {?Node} */ | |
| this.parent; | |
| /** @type {string} */ | |
| this._groupId = ''; | |
| this._isGroupNode = false; | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| isGroupNode() { | |
| return this._isGroupNode; | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| hasChildren() { | |
| throw 'Not implemented'; | |
| } | |
| /** | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| children() { | |
| throw 'Not implemented'; | |
| } | |
| /** | |
| * @param {function(!SDK.TracingModel.Event):boolean} matchFunction | |
| * @param {!Array<!Node>=} results | |
| * @return {!Array<!Node>} | |
| */ | |
| searchTree(matchFunction, results) { | |
| results = results || []; | |
| if (this.event && matchFunction(this.event)) { | |
| results.push(this); | |
| } | |
| for (const child of this.children().values()) { | |
| child.searchTree(matchFunction, results); | |
| } | |
| return results; | |
| } | |
| } | |
| export class TopDownNode extends Node { | |
| /** | |
| * @param {string|symbol} id | |
| * @param {?SDK.TracingModel.Event} event | |
| * @param {?TopDownNode} parent | |
| */ | |
| constructor(id, event, parent) { | |
| super(id, event); | |
| /** @type {?TopDownRootNode} */ | |
| this._root = parent && parent._root; | |
| this._hasChildren = false; | |
| this._children = null; | |
| this.parent = parent; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| hasChildren() { | |
| return this._hasChildren; | |
| } | |
| /** | |
| * @override | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| children() { | |
| return this._children || this._buildChildren(); | |
| } | |
| /** | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| _buildChildren() { | |
| /** @type {!Array<!TopDownNode>} */ | |
| const path = []; | |
| for (let node = this; node.parent && !node._isGroupNode; node = node.parent) { | |
| path.push(/** @type {!TopDownNode} */ (node)); | |
| } | |
| path.reverse(); | |
| /** @type {!TimelineModel.TimelineProfileTree.ChildrenCache} */ | |
| const children = new Map(); | |
| const self = this; | |
| const root = this._root; | |
| const startTime = root._startTime; | |
| const endTime = root._endTime; | |
| const instantEventCallback = root._doNotAggregate ? onInstantEvent : undefined; | |
| const eventIdCallback = root._doNotAggregate ? undefined : _eventId; | |
| const eventGroupIdCallback = root._eventGroupIdCallback; | |
| let depth = 0; | |
| let matchedDepth = 0; | |
| let currentDirectChild = null; | |
| TimelineModel.TimelineModel.forEachEvent( | |
| root._events, onStartEvent, onEndEvent, instantEventCallback, startTime, endTime, root._filter); | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| */ | |
| function onStartEvent(e) { | |
| ++depth; | |
| if (depth > path.length + 2) { | |
| return; | |
| } | |
| if (!matchPath(e)) { | |
| return; | |
| } | |
| const duration = Math.min(endTime, e.endTime) - Math.max(startTime, e.startTime); | |
| if (duration < 0) { | |
| console.error('Negative event duration'); | |
| } | |
| processEvent(e, duration); | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| */ | |
| function onInstantEvent(e) { | |
| ++depth; | |
| if (matchedDepth === path.length && depth <= path.length + 2) { | |
| processEvent(e, 0); | |
| } | |
| --depth; | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| * @param {number} duration | |
| */ | |
| function processEvent(e, duration) { | |
| if (depth === path.length + 2) { | |
| currentDirectChild._hasChildren = true; | |
| currentDirectChild.selfTime -= duration; | |
| return; | |
| } | |
| let id; | |
| let groupId = ''; | |
| if (!eventIdCallback) { | |
| id = Symbol('uniqueId'); | |
| } else { | |
| id = eventIdCallback(e); | |
| groupId = eventGroupIdCallback ? eventGroupIdCallback(e) : ''; | |
| if (groupId) { | |
| id += '/' + groupId; | |
| } | |
| } | |
| let node = children.get(id); | |
| if (!node) { | |
| node = new TopDownNode(id, e, self); | |
| node._groupId = groupId; | |
| children.set(id, node); | |
| } | |
| node.selfTime += duration; | |
| node.totalTime += duration; | |
| currentDirectChild = node; | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| * @return {boolean} | |
| */ | |
| function matchPath(e) { | |
| if (matchedDepth === path.length) { | |
| return true; | |
| } | |
| if (matchedDepth !== depth - 1) { | |
| return false; | |
| } | |
| if (!e.endTime) { | |
| return false; | |
| } | |
| if (!eventIdCallback) { | |
| if (e === path[matchedDepth].event) { | |
| ++matchedDepth; | |
| } | |
| return false; | |
| } | |
| let id = eventIdCallback(e); | |
| const groupId = eventGroupIdCallback ? eventGroupIdCallback(e) : ''; | |
| if (groupId) { | |
| id += '/' + groupId; | |
| } | |
| if (id === path[matchedDepth].id) { | |
| ++matchedDepth; | |
| } | |
| return false; | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| */ | |
| function onEndEvent(e) { | |
| --depth; | |
| if (matchedDepth > depth) { | |
| matchedDepth = depth; | |
| } | |
| } | |
| this._children = children; | |
| return children; | |
| } | |
| } | |
| export class TopDownRootNode extends TopDownNode { | |
| /** | |
| * @param {!Array<!SDK.TracingModel.Event>} events | |
| * @param {!Array<!TimelineModel.TimelineModelFilter>} filters | |
| * @param {number} startTime | |
| * @param {number} endTime | |
| * @param {boolean=} doNotAggregate | |
| * @param {?function(!SDK.TracingModel.Event):string=} eventGroupIdCallback | |
| */ | |
| constructor(events, filters, startTime, endTime, doNotAggregate, eventGroupIdCallback) { | |
| super('', null, null); | |
| this._root = this; | |
| this._events = events; | |
| this._filter = e => filters.every(f => f.accept(e)); | |
| this._startTime = startTime; | |
| this._endTime = endTime; | |
| this._eventGroupIdCallback = eventGroupIdCallback; | |
| this._doNotAggregate = doNotAggregate; | |
| this.totalTime = endTime - startTime; | |
| this.selfTime = this.totalTime; | |
| } | |
| /** | |
| * @override | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| children() { | |
| return this._children || this._grouppedTopNodes(); | |
| } | |
| /** | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| _grouppedTopNodes() { | |
| const flatNodes = super.children(); | |
| for (const node of flatNodes.values()) { | |
| this.selfTime -= node.totalTime; | |
| } | |
| if (!this._eventGroupIdCallback) { | |
| return flatNodes; | |
| } | |
| const groupNodes = new Map(); | |
| for (const node of flatNodes.values()) { | |
| const groupId = this._eventGroupIdCallback(/** @type {!SDK.TracingModel.Event} */ (node.event)); | |
| let groupNode = groupNodes.get(groupId); | |
| if (!groupNode) { | |
| groupNode = new GroupNode(groupId, this, /** @type {!SDK.TracingModel.Event} */ (node.event)); | |
| groupNodes.set(groupId, groupNode); | |
| } | |
| groupNode.addChild(node, node.selfTime, node.totalTime); | |
| } | |
| this._children = groupNodes; | |
| return groupNodes; | |
| } | |
| } | |
| export class BottomUpRootNode extends Node { | |
| /** | |
| * @param {!Array<!SDK.TracingModel.Event>} events | |
| * @param {!TimelineModel.TimelineModelFilter} textFilter | |
| * @param {!Array<!TimelineModel.TimelineModelFilter>} filters | |
| * @param {number} startTime | |
| * @param {number} endTime | |
| * @param {?function(!SDK.TracingModel.Event):string} eventGroupIdCallback | |
| */ | |
| constructor(events, textFilter, filters, startTime, endTime, eventGroupIdCallback) { | |
| super('', null); | |
| /** @type {?TimelineModel.TimelineProfileTree.ChildrenCache} */ | |
| this._children = null; | |
| this._events = events; | |
| this._textFilter = textFilter; | |
| this._filter = e => filters.every(f => f.accept(e)); | |
| this._startTime = startTime; | |
| this._endTime = endTime; | |
| this._eventGroupIdCallback = eventGroupIdCallback; | |
| this.totalTime = endTime - startTime; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| hasChildren() { | |
| return true; | |
| } | |
| /** | |
| * @param {!TimelineModel.TimelineProfileTree.ChildrenCache} children | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| _filterChildren(children) { | |
| for (const [id, child] of children) { | |
| if (child.event && !this._textFilter.accept(child.event)) { | |
| children.delete(/** @type {string|symbol} */ (id)); | |
| } | |
| } | |
| return children; | |
| } | |
| /** | |
| * @override | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| children() { | |
| if (!this._children) { | |
| this._children = this._filterChildren(this._grouppedTopNodes()); | |
| } | |
| return this._children; | |
| } | |
| /** | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| _ungrouppedTopNodes() { | |
| const root = this; | |
| const startTime = this._startTime; | |
| const endTime = this._endTime; | |
| /** @type {!TimelineModel.TimelineProfileTree.ChildrenCache} */ | |
| const nodeById = new Map(); | |
| /** @type {!Array<number>} */ | |
| const selfTimeStack = [endTime - startTime]; | |
| /** @type {!Array<boolean>} */ | |
| const firstNodeStack = []; | |
| /** @type {!Map<string, number>} */ | |
| const totalTimeById = new Map(); | |
| TimelineModel.TimelineModel.forEachEvent( | |
| this._events, onStartEvent, onEndEvent, undefined, startTime, endTime, this._filter); | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| */ | |
| function onStartEvent(e) { | |
| const duration = Math.min(e.endTime, endTime) - Math.max(e.startTime, startTime); | |
| selfTimeStack[selfTimeStack.length - 1] -= duration; | |
| selfTimeStack.push(duration); | |
| const id = _eventId(e); | |
| const noNodeOnStack = !totalTimeById.has(id); | |
| if (noNodeOnStack) { | |
| totalTimeById.set(id, duration); | |
| } | |
| firstNodeStack.push(noNodeOnStack); | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| */ | |
| function onEndEvent(e) { | |
| const id = _eventId(e); | |
| let node = nodeById.get(id); | |
| if (!node) { | |
| node = new BottomUpNode(root, id, e, false, root); | |
| nodeById.set(id, node); | |
| } | |
| node.selfTime += selfTimeStack.pop(); | |
| if (firstNodeStack.pop()) { | |
| node.totalTime += totalTimeById.get(id); | |
| totalTimeById.delete(id); | |
| } | |
| if (firstNodeStack.length) { | |
| node.setHasChildren(); | |
| } | |
| } | |
| this.selfTime = selfTimeStack.pop(); | |
| for (const pair of nodeById) { | |
| if (pair[1].selfTime <= 0) { | |
| nodeById.delete(/** @type {string} */ (pair[0])); | |
| } | |
| } | |
| return nodeById; | |
| } | |
| /** | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| _grouppedTopNodes() { | |
| const flatNodes = this._ungrouppedTopNodes(); | |
| if (!this._eventGroupIdCallback) { | |
| return flatNodes; | |
| } | |
| const groupNodes = new Map(); | |
| for (const node of flatNodes.values()) { | |
| const groupId = this._eventGroupIdCallback(/** @type {!SDK.TracingModel.Event} */ (node.event)); | |
| let groupNode = groupNodes.get(groupId); | |
| if (!groupNode) { | |
| groupNode = new GroupNode(groupId, this, /** @type {!SDK.TracingModel.Event} */ (node.event)); | |
| groupNodes.set(groupId, groupNode); | |
| } | |
| groupNode.addChild(node, node.selfTime, node.selfTime); | |
| } | |
| return groupNodes; | |
| } | |
| } | |
| export class GroupNode extends Node { | |
| /** | |
| * @param {string} id | |
| * @param {!BottomUpRootNode|!TopDownRootNode} parent | |
| * @param {!SDK.TracingModel.Event} event | |
| */ | |
| constructor(id, parent, event) { | |
| super(id, event); | |
| this._children = new Map(); | |
| this.parent = parent; | |
| this._isGroupNode = true; | |
| } | |
| /** | |
| * @param {!BottomUpNode} child | |
| * @param {number} selfTime | |
| * @param {number} totalTime | |
| */ | |
| addChild(child, selfTime, totalTime) { | |
| this._children.set(child.id, child); | |
| this.selfTime += selfTime; | |
| this.totalTime += totalTime; | |
| child.parent = this; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| hasChildren() { | |
| return true; | |
| } | |
| /** | |
| * @override | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| children() { | |
| return this._children; | |
| } | |
| } | |
| export class BottomUpNode extends Node { | |
| /** | |
| * @param {!BottomUpRootNode} root | |
| * @param {string} id | |
| * @param {!SDK.TracingModel.Event} event | |
| * @param {boolean} hasChildren | |
| * @param {!Node} parent | |
| */ | |
| constructor(root, id, event, hasChildren, parent) { | |
| super(id, event); | |
| this.parent = parent; | |
| this._root = root; | |
| this._depth = (parent._depth || 0) + 1; | |
| /** @type {?TimelineModel.TimelineProfileTree.ChildrenCache} */ | |
| this._cachedChildren = null; | |
| this._hasChildren = hasChildren; | |
| } | |
| setHasChildren() { | |
| this._hasChildren = true; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| hasChildren() { | |
| return this._hasChildren; | |
| } | |
| /** | |
| * @override | |
| * @return {!TimelineModel.TimelineProfileTree.ChildrenCache} | |
| */ | |
| children() { | |
| if (this._cachedChildren) { | |
| return this._cachedChildren; | |
| } | |
| /** @type {!Array<number>} */ | |
| const selfTimeStack = [0]; | |
| /** @type {!Array<string>} */ | |
| const eventIdStack = []; | |
| /** @type {!Array<!SDK.TracingModel.Event>} */ | |
| const eventStack = []; | |
| /** @type {!TimelineModel.TimelineProfileTree.ChildrenCache} */ | |
| const nodeById = new Map(); | |
| const startTime = this._root._startTime; | |
| const endTime = this._root._endTime; | |
| let lastTimeMarker = startTime; | |
| const self = this; | |
| TimelineModel.TimelineModel.forEachEvent( | |
| this._root._events, onStartEvent, onEndEvent, undefined, startTime, endTime, this._root._filter); | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| */ | |
| function onStartEvent(e) { | |
| const duration = Math.min(e.endTime, endTime) - Math.max(e.startTime, startTime); | |
| if (duration < 0) { | |
| console.assert(false, 'Negative duration of an event'); | |
| } | |
| selfTimeStack[selfTimeStack.length - 1] -= duration; | |
| selfTimeStack.push(duration); | |
| const id = _eventId(e); | |
| eventIdStack.push(id); | |
| eventStack.push(e); | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} e | |
| */ | |
| function onEndEvent(e) { | |
| const selfTime = selfTimeStack.pop(); | |
| const id = eventIdStack.pop(); | |
| eventStack.pop(); | |
| let node; | |
| for (node = self; node._depth > 1; node = node.parent) { | |
| if (node.id !== eventIdStack[eventIdStack.length + 1 - node._depth]) { | |
| return; | |
| } | |
| } | |
| if (node.id !== id || eventIdStack.length < self._depth) { | |
| return; | |
| } | |
| const childId = eventIdStack[eventIdStack.length - self._depth]; | |
| node = nodeById.get(childId); | |
| if (!node) { | |
| const event = eventStack[eventStack.length - self._depth]; | |
| const hasChildren = eventStack.length > self._depth; | |
| node = new BottomUpNode(self._root, childId, event, hasChildren, self); | |
| nodeById.set(childId, node); | |
| } | |
| const totalTime = Math.min(e.endTime, endTime) - Math.max(e.startTime, lastTimeMarker); | |
| node.selfTime += selfTime; | |
| node.totalTime += totalTime; | |
| lastTimeMarker = Math.min(e.endTime, endTime); | |
| } | |
| this._cachedChildren = this._root._filterChildren(nodeById); | |
| return this._cachedChildren; | |
| } | |
| /** | |
| * @override | |
| * @param {function(!SDK.TracingModel.Event):boolean} matchFunction | |
| * @param {!Array<!Node>=} results | |
| * @return {!Array<!Node>} | |
| */ | |
| searchTree(matchFunction, results) { | |
| results = results || []; | |
| if (this.event && matchFunction(this.event)) { | |
| results.push(this); | |
| } | |
| return results; | |
| } | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} event | |
| * @return {?string} | |
| */ | |
| export function eventURL(event) { | |
| const data = event.args['data'] || event.args['beginData']; | |
| if (data && data['url']) { | |
| return data['url']; | |
| } | |
| let frame = eventStackFrame(event); | |
| while (frame) { | |
| const url = frame['url']; | |
| if (url) { | |
| return url; | |
| } | |
| frame = frame.parent; | |
| } | |
| return null; | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} event | |
| * @return {?Protocol.Runtime.CallFrame} | |
| */ | |
| export function eventStackFrame(event) { | |
| if (event.name === TimelineModel.TimelineModel.RecordType.JSFrame) { | |
| return /** @type {?Protocol.Runtime.CallFrame} */ (event.args['data'] || null); | |
| } | |
| return TimelineModel.TimelineData.forEvent(event).topFrame(); | |
| } | |
| /** | |
| * @param {!SDK.TracingModel.Event} event | |
| * @return {string} | |
| */ | |
| export function _eventId(event) { | |
| if (event.name === TimelineModel.TimelineModel.RecordType.TimeStamp) { | |
| return `${event.name}:${event.args.data.message}`; | |
| } | |
| if (event.name !== TimelineModel.TimelineModel.RecordType.JSFrame) { | |
| return event.name; | |
| } | |
| const frame = event.args['data']; | |
| const location = frame['scriptId'] || frame['url'] || ''; | |
| const functionName = frame['functionName']; | |
| const name = TimelineModel.TimelineJSProfileProcessor.isNativeRuntimeFrame(frame) ? | |
| TimelineModel.TimelineJSProfileProcessor.nativeGroup(functionName) || functionName : | |
| `${functionName}:${frame['lineNumber']}:${frame['columnNumber']}`; | |
| return `f:${name}@${location}`; | |
| } | |
| /* Legacy exported object */ | |
| self.TimelineModel = self.TimelineModel || {}; | |
| /* Legacy exported object */ | |
| TimelineModel = TimelineModel || {}; | |
| TimelineModel.TimelineProfileTree = {}; | |
| /** @constructor */ | |
| TimelineModel.TimelineProfileTree.Node = Node; | |
| /** @constructor */ | |
| TimelineModel.TimelineProfileTree.TopDownNode = TopDownNode; | |
| /** @constructor */ | |
| TimelineModel.TimelineProfileTree.TopDownRootNode = TopDownRootNode; | |
| /** @constructor */ | |
| TimelineModel.TimelineProfileTree.BottomUpRootNode = BottomUpRootNode; | |
| /** @constructor */ | |
| TimelineModel.TimelineProfileTree.GroupNode = GroupNode; | |
| /** @constructor */ | |
| TimelineModel.TimelineProfileTree.BottomUpNode = BottomUpNode; | |
| TimelineModel.TimelineProfileTree.eventURL = eventURL; | |
| TimelineModel.TimelineProfileTree.eventStackFrame = eventStackFrame; | |
| TimelineModel.TimelineProfileTree._eventId = _eventId; | |
| /** | |
| * @typedef {Map<string|symbol, !Node>} | |
| */ | |
| TimelineModel.TimelineProfileTree.ChildrenCache; |