UNPKG

chrome-devtools-frontend

Version:
502 lines (467 loc) • 19.4 kB
// Copyright 2014 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. import * as Common from '../common/common.js'; import * as Platform from '../platform/platform.js'; import type * as Protocol from '../../generated/protocol.js'; import {ProfileNode, ProfileTreeModel} from './ProfileTreeModel.js'; import {type Target} from './Target.js'; export class CPUProfileNode extends ProfileNode { override id: number; override self: number; positionTicks: Protocol.Profiler.PositionTickInfo[]|undefined; override deoptReason: string|null; constructor(node: Protocol.Profiler.ProfileNode, sampleTime: number, target: Target|null) { const callFrame = node.callFrame || ({ // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error functionName: node['functionName'], // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error scriptId: node['scriptId'], // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error url: node['url'], // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error lineNumber: node['lineNumber'] - 1, // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error columnNumber: node['columnNumber'] - 1, } as Protocol.Runtime.CallFrame); super(callFrame, target); this.id = node.id; this.self = (node.hitCount || 0) * sampleTime; this.positionTicks = node.positionTicks; // Compatibility: legacy backends could provide "no reason" for optimized functions. this.deoptReason = node.deoptReason && node.deoptReason !== 'no reason' ? node.deoptReason : null; } } export class CPUProfileDataModel extends ProfileTreeModel { profileStartTime: number; profileEndTime: number; timestamps: number[]; samples: number[]|undefined; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any lines: any; totalHitCount: number; profileHead: CPUProfileNode; /** * A cache for the nodes we have parsed. * Note: "Parsed" nodes are different from the "Protocol" nodes, the * latter being the raw data we receive from the backend. */ #idToParsedNode!: Map<number, CPUProfileNode>; gcNode!: CPUProfileNode; programNode?: ProfileNode; idleNode?: ProfileNode; #stackStartTimes?: Float64Array; #stackChildrenDuration?: Float64Array; constructor(profile: Protocol.Profiler.Profile, target: Target|null) { super(target); // @ts-ignore Legacy types const isLegacyFormat = Boolean(profile['head']); if (isLegacyFormat) { // Legacy format contains raw timestamps and start/stop times are in seconds. this.profileStartTime = profile.startTime * 1000; this.profileEndTime = profile.endTime * 1000; // @ts-ignore Legacy types this.timestamps = profile.timestamps; this.compatibilityConversionHeadToNodes(profile); } else { // Current format encodes timestamps as deltas. Start/stop times are in microseconds. this.profileStartTime = profile.startTime / 1000; this.profileEndTime = profile.endTime / 1000; this.timestamps = this.convertTimeDeltas(profile); } this.samples = profile.samples; // @ts-ignore Legacy types this.lines = profile.lines; this.totalHitCount = 0; this.profileHead = this.translateProfileTree(profile.nodes); this.initialize(this.profileHead); this.extractMetaNodes(); if (this.samples) { this.sortSamples(); this.normalizeTimestamps(); this.fixMissingSamples(); } } private compatibilityConversionHeadToNodes(profile: Protocol.Profiler.Profile): void { // @ts-ignore Legacy types if (!profile.head || profile.nodes) { return; } const nodes: Protocol.Profiler.ProfileNode[] = []; // @ts-ignore Legacy types convertNodesTree(profile.head); profile.nodes = nodes; // @ts-ignore Legacy types delete profile.head; function convertNodesTree(node: Protocol.Profiler.ProfileNode): number { nodes.push(node); // @ts-ignore Legacy types node.children = (node.children as Protocol.Profiler.ProfileNode[]).map(convertNodesTree); return node.id; } } private convertTimeDeltas(profile: Protocol.Profiler.Profile): number[] { if (!profile.timeDeltas) { return []; } let lastTimeMicroSec = profile.startTime; const timestamps = new Array(profile.timeDeltas.length); for (let i = 0; i < profile.timeDeltas.length; ++i) { lastTimeMicroSec += profile.timeDeltas[i]; timestamps[i] = lastTimeMicroSec; } return timestamps; } /** * Creates a Tree of CPUProfileNodes using the Protocol.Profiler.ProfileNodes. * As the tree is built, samples of native code (prefixed with "native ") are * filtered out. Samples of filtered nodes are replaced with the parent of the * node being filtered. * * This function supports legacy and new definitions of the CDP Profiler.Profile * type as well as the type of a CPU profile contained in trace events. */ private translateProfileTree(nodes: Protocol.Profiler.ProfileNode[]): CPUProfileNode { function isNativeNode(node: Protocol.Profiler.ProfileNode): boolean { if (node.callFrame) { return Boolean(node.callFrame.url) && node.callFrame.url.startsWith('native '); } // @ts-ignore Legacy types return Boolean(node['url']) && node['url'].startsWith('native '); } function buildChildrenFromParents(nodes: Protocol.Profiler.ProfileNode[]): void { if (nodes[0].children) { return; } nodes[0].children = []; for (let i = 1; i < nodes.length; ++i) { const node = nodes[i]; // @ts-ignore Legacy types const parentNode = protocolNodeById.get(node.parent); // @ts-ignore Legacy types if (parentNode.children) { // @ts-ignore Legacy types parentNode.children.push(node.id); } else { // @ts-ignore Legacy types parentNode.children = [node.id]; } } } /** * Calculate how many times each node was sampled in the profile, if * not available in the profile data. */ function buildHitCountFromSamples(nodes: Protocol.Profiler.ProfileNode[], samples: number[]|undefined): void { // If hit count is available, this profile has the new format, so // no need to continue.` if (typeof (nodes[0].hitCount) === 'number') { return; } if (!samples) { throw new Error('Error: Neither hitCount nor samples are present in profile.'); } for (let i = 0; i < nodes.length; ++i) { nodes[i].hitCount = 0; } for (let i = 0; i < samples.length; ++i) { const node = protocolNodeById.get(samples[i]); if (!node || node.hitCount === undefined) { continue; } node.hitCount++; } } // A cache for the raw nodes received from the traces / CDP. const protocolNodeById = new Map<number, Protocol.Profiler.ProfileNode>(); for (let i = 0; i < nodes.length; ++i) { const node = nodes[i]; protocolNodeById.set(node.id, node); } buildHitCountFromSamples(nodes, this.samples); buildChildrenFromParents(nodes); this.totalHitCount = nodes.reduce((acc, node) => acc + (node.hitCount || 0), 0); const sampleTime = (this.profileEndTime - this.profileStartTime) / this.totalHitCount; const keepNatives = Boolean(Common.Settings.Settings.instance().moduleSetting('showNativeFunctionsInJSProfile').get()); const root = nodes[0]; // If a node is filtered out, its samples are replaced with its parent, // so we keep track of the which id to use in the samples data. const idToUseForRemovedNode = new Map<number, number>([[root.id, root.id]]); this.#idToParsedNode = new Map(); const resultRoot = new CPUProfileNode(root, sampleTime, this.target()); this.#idToParsedNode.set(root.id, resultRoot); if (!root.children) { throw new Error('Missing children for root'); } const parentNodeStack = root.children.map(() => resultRoot); const sourceNodeStack = root.children.map(id => protocolNodeById.get(id)); while (sourceNodeStack.length) { let parentNode = parentNodeStack.pop(); const sourceNode = sourceNodeStack.pop(); if (!sourceNode || !parentNode) { continue; } if (!sourceNode.children) { sourceNode.children = []; } const targetNode = new CPUProfileNode(sourceNode, sampleTime, this.target()); if (keepNatives || !isNativeNode(sourceNode)) { parentNode.children.push(targetNode); parentNode = targetNode; } else { parentNode.self += targetNode.self; } idToUseForRemovedNode.set(sourceNode.id, parentNode.id); parentNodeStack.push.apply(parentNodeStack, sourceNode.children.map(() => parentNode as CPUProfileNode)); sourceNodeStack.push.apply(sourceNodeStack, sourceNode.children.map(id => protocolNodeById.get(id))); this.#idToParsedNode.set(sourceNode.id, targetNode); } if (this.samples) { this.samples = this.samples.map(id => idToUseForRemovedNode.get(id) as number); } return resultRoot; } /** * Sorts the samples array using the timestamps array (there is a one * to one matching by index between the two). */ private sortSamples(): void { if (!this.timestamps || !this.samples) { return; } const timestamps = this.timestamps; const samples = this.samples; const orderedIndices = timestamps.map((_x, index) => index); orderedIndices.sort((a, b) => timestamps[a] - timestamps[b]); this.timestamps = []; this.samples = []; for (let i = 0; i < orderedIndices.length; i++) { const orderedIndex = orderedIndices[i]; this.timestamps.push(timestamps[orderedIndex]); this.samples.push(samples[orderedIndex]); } } /** * Fills in timestamps and/or time deltas from legacy profiles where * they could be missing. */ private normalizeTimestamps(): void { if (!this.samples) { return; } let timestamps: number[] = this.timestamps; if (!timestamps) { // Support loading old CPU profiles that are missing timestamps. // Derive timestamps from profile start and stop times. const profileStartTime = this.profileStartTime; const interval = (this.profileEndTime - profileStartTime) / this.samples.length; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any timestamps = (new Float64Array(this.samples.length + 1) as any); for (let i = 0; i < timestamps.length; ++i) { timestamps[i] = profileStartTime + i * interval; } this.timestamps = timestamps; return; } // Convert samples from micro to milliseconds for (let i = 0; i < timestamps.length; ++i) { timestamps[i] /= 1000; } if (this.samples.length === timestamps.length) { // Support for a legacy format where there are no timeDeltas. // Add an extra timestamp used to calculate the last sample duration. const lastTimestamp = timestamps.at(-1) || 0; const averageIntervalTime = (lastTimestamp - timestamps[0]) / (timestamps.length - 1); this.timestamps.push(lastTimestamp + averageIntervalTime); } this.profileStartTime = timestamps.at(0) || this.profileStartTime; this.profileEndTime = timestamps.at(-1) || this.profileEndTime; } private extractMetaNodes(): void { const topLevelNodes = this.profileHead.children; for (let i = 0; i < topLevelNodes.length && !(this.gcNode && this.programNode && this.idleNode); i++) { const node = topLevelNodes[i]; if (node.functionName === '(garbage collector)') { this.gcNode = (node as CPUProfileNode); } else if (node.functionName === '(program)') { this.programNode = node; } else if (node.functionName === '(idle)') { this.idleNode = node; } } } private fixMissingSamples(): void { // Sometimes the V8 sampler is not able to parse the JS stack and returns // a (program) sample instead. The issue leads to call frames being split // apart when they shouldn't. // Here's a workaround for that. When there's a single (program) sample // between two call stacks sharing the same bottom node, it is replaced // with the preceeding sample. const samples = this.samples; if (!samples) { return; } const samplesCount = samples.length; if (!this.programNode || samplesCount < 3) { return; } const idToNode = this.#idToParsedNode; const programNodeId = this.programNode.id; const gcNodeId = this.gcNode ? this.gcNode.id : -1; const idleNodeId = this.idleNode ? this.idleNode.id : -1; let prevNodeId: number = samples[0]; let nodeId: number = samples[1]; for (let sampleIndex = 1; sampleIndex < samplesCount - 1; sampleIndex++) { const nextNodeId = samples[sampleIndex + 1]; if (nodeId === programNodeId && !isSystemNode(prevNodeId) && !isSystemNode(nextNodeId) && bottomNode((idToNode.get(prevNodeId) as ProfileNode)) === bottomNode((idToNode.get(nextNodeId) as ProfileNode))) { samples[sampleIndex] = prevNodeId; } prevNodeId = nodeId; nodeId = nextNodeId; } function bottomNode(node: ProfileNode): ProfileNode { while (node.parent && node.parent.parent) { node = node.parent; } return node; } function isSystemNode(nodeId: number): boolean { return nodeId === programNodeId || nodeId === gcNodeId || nodeId === idleNodeId; } } forEachFrame( openFrameCallback: (arg0: number, arg1: CPUProfileNode, arg2: number) => void, closeFrameCallback: (arg0: number, arg1: CPUProfileNode, arg2: number, arg3: number, arg4: number) => void, startTime?: number, stopTime?: number): void { if (!this.profileHead || !this.samples) { return; } startTime = startTime || 0; stopTime = stopTime || Infinity; const samples = this.samples; const timestamps = this.timestamps; const idToNode = this.#idToParsedNode; const gcNode = this.gcNode; const samplesCount = samples.length; const startIndex = Platform.ArrayUtilities.lowerBound(timestamps, startTime, Platform.ArrayUtilities.DEFAULT_COMPARATOR); let stackTop = 0; const stackNodes = []; let prevId: number = this.profileHead.id; let sampleTime; let gcParentNode: CPUProfileNode|null = null; // Extra slots for gc being put on top, // and one at the bottom to allow safe stackTop-1 access. const stackDepth = this.maxDepth + 3; if (!this.#stackStartTimes) { this.#stackStartTimes = new Float64Array(stackDepth); } const stackStartTimes = this.#stackStartTimes; if (!this.#stackChildrenDuration) { this.#stackChildrenDuration = new Float64Array(stackDepth); } const stackChildrenDuration = this.#stackChildrenDuration; let node; let sampleIndex; for (sampleIndex = startIndex; sampleIndex < samplesCount; sampleIndex++) { sampleTime = timestamps[sampleIndex]; if (sampleTime >= stopTime) { break; } const id = samples[sampleIndex]; if (id === prevId) { continue; } node = idToNode.get(id); let prevNode: CPUProfileNode = (idToNode.get(prevId) as CPUProfileNode); if (node === gcNode) { // GC samples have no stack, so we just put GC node on top of the last recorded sample. gcParentNode = prevNode; openFrameCallback(gcParentNode.depth + 1, gcNode, sampleTime); stackStartTimes[++stackTop] = sampleTime; stackChildrenDuration[stackTop] = 0; prevId = id; continue; } if (prevNode === gcNode && gcParentNode) { // end of GC frame const start = stackStartTimes[stackTop]; const duration = sampleTime - start; stackChildrenDuration[stackTop - 1] += duration; closeFrameCallback(gcParentNode.depth + 1, gcNode, start, duration, duration - stackChildrenDuration[stackTop]); --stackTop; prevNode = gcParentNode; prevId = prevNode.id; gcParentNode = null; } while (node && node.depth > prevNode.depth) { stackNodes.push(node); node = node.parent; } // Go down to the LCA and close current intervals. while (prevNode !== node) { const start = stackStartTimes[stackTop]; const duration = sampleTime - start; stackChildrenDuration[stackTop - 1] += duration; closeFrameCallback( prevNode.depth, (prevNode as CPUProfileNode), start, duration, duration - stackChildrenDuration[stackTop]); --stackTop; if (node && node.depth === prevNode.depth) { stackNodes.push(node); node = node.parent; } prevNode = (prevNode.parent as CPUProfileNode); } // Go up the nodes stack and open new intervals. while (stackNodes.length) { const currentNode = (stackNodes.pop() as CPUProfileNode); node = currentNode; openFrameCallback(currentNode.depth, currentNode, sampleTime); stackStartTimes[++stackTop] = sampleTime; stackChildrenDuration[stackTop] = 0; } prevId = id; } sampleTime = timestamps[sampleIndex] || this.profileEndTime; if (gcParentNode && idToNode.get(prevId) === gcNode) { const start = stackStartTimes[stackTop]; const duration = sampleTime - start; stackChildrenDuration[stackTop - 1] += duration; closeFrameCallback( gcParentNode.depth + 1, (node as CPUProfileNode), start, duration, duration - stackChildrenDuration[stackTop]); --stackTop; prevId = gcParentNode.id; } for (let node = idToNode.get(prevId); node && node.parent; node = (node.parent as CPUProfileNode)) { const start = stackStartTimes[stackTop]; const duration = sampleTime - start; stackChildrenDuration[stackTop - 1] += duration; closeFrameCallback( node.depth, (node as CPUProfileNode), start, duration, duration - stackChildrenDuration[stackTop]); --stackTop; } } /** * Returns the node that corresponds to a given index of a sample. */ nodeByIndex(index: number): CPUProfileNode|null { return this.samples && this.#idToParsedNode.get(this.samples[index]) || null; } nodes(): CPUProfileNode[]|null { if (!this.#idToParsedNode) { return null; } return [...this.#idToParsedNode.values()]; } }