speedscope/import/chrome.ts

170 lines
4.6 KiB
TypeScript

import {Profile, TimeFormatter, FrameInfo} from '../profile'
import {getOrInsert, lastOf} from '../utils'
interface TimelineEvent {
pid: number,
tid: number,
ts: number,
ph: string,
cat: string,
name: string,
dur: number,
tdur: number,
tts: number,
args: { [key: string]: any }
}
interface PositionTickInfo {
line: number,
ticks: number
}
interface CPUProfileCallFrame {
columnNumber: number,
functionName: string,
lineNumber: number,
scriptId: string,
url: string
}
interface CPUProfileNode {
callFrame: CPUProfileCallFrame
hitCount: number
id: number
children?: number[]
positionTicks?: PositionTickInfo[]
parent?: CPUProfileNode
}
interface CPUProfile {
startTime: number,
endTime: number,
nodes: CPUProfileNode[],
samples: number[],
timeDeltas: number[]
}
export function importFromChromeTimeline(events: TimelineEvent[]) {
// It seems like sometimes Chrome timeline files contain multiple CpuProfiles?
// For now, choose the first one in the list.
for (let event of events) {
if (event.name == "CpuProfile") {
const chromeProfile = event.args.data.cpuProfile as CPUProfile
return importFromChromeCPUProfile(chromeProfile)
}
}
throw new Error("Could not find CPU profile in Timeline")
}
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
function frameInfoForCallFrame(callFrame: CPUProfileCallFrame) {
return getOrInsert(callFrameToFrameInfo, callFrame, (callFrame) => {
const name = callFrame.functionName || "(anonymous)"
const file = callFrame.url
const line = callFrame.lineNumber
const col = callFrame.columnNumber
return {
key: `${name}:${file}:${line}:${col}`,
name,
file,
line,
col
}
})
}
export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
const profile = new Profile(chromeProfile.endTime - chromeProfile.startTime)
const nodeById = new Map<number, CPUProfileNode>()
for (let node of chromeProfile.nodes) {
nodeById.set(node.id, node)
}
for (let node of chromeProfile.nodes) {
if (!node.children) continue
for (let childId of node.children) {
const child = nodeById.get(childId)
if (!child) continue
child.parent = node
}
}
const samples: number[] = []
const timeDeltas: number[] = []
let elapsed = 0
let lastNodeId = NaN
// The chrome CPU profile format doesn't collapse identical samples. We'll do that
// here to save a ton of work later doing mergers.
for (let i = 0; i < chromeProfile.samples.length; i++) {
const nodeId = chromeProfile.samples[i]
if (nodeId != lastNodeId) {
samples.push(nodeId)
timeDeltas.push(elapsed)
elapsed = 0
}
elapsed += chromeProfile.timeDeltas[i]
lastNodeId = nodeId
}
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId)
timeDeltas.push(elapsed)
}
let prevStack: CPUProfileNode[] = []
let value = 0
for (let i = 0; i < samples.length; i++) {
const timeDelta = timeDeltas[i+1] || 0
const nodeId = samples[i]
let stackTop = nodeById.get(nodeId)
if (!stackTop) continue
// Find lowest common ancestor of the current stack and the previous one
let lca: CPUProfileNode | null = null
// This is O(n^2), but n should be relatively small here (stack height),
// so hopefully this isn't much of a problem
for (
lca = stackTop;
lca && prevStack.indexOf(lca) === -1;
lca = lca.callFrame.functionName === "(garbage collector)" ? lastOf(prevStack) : lca.parent || null
) {}
// Close frames that are no longer open
while (prevStack.length > 0 && lastOf(prevStack) != lca) {
const closingNode = prevStack.pop()!
const frame = frameInfoForCallFrame(closingNode.callFrame)
profile.leaveFrame(frame, value)
}
// Open frames that are now becoming open
const toOpen: CPUProfileNode[] = []
for (
let node: CPUProfileNode | null = stackTop;
node && node != lca;
// Place GC calls on top of the previous call stack
node = node.callFrame.functionName === "(garbage collector)" ? lastOf(prevStack) : node.parent || null
) {
toOpen.push(node)
}
toOpen.reverse()
for (let node of toOpen) {
profile.enterFrame(frameInfoForCallFrame(node.callFrame), value)
}
prevStack = prevStack.concat(toOpen)
value += timeDelta
}
// Close frames that are open at the end of the trace
for (let i = prevStack.length - 1; i >= 0; i--) {
profile.leaveFrame(frameInfoForCallFrame(prevStack[i].callFrame), value)
}
profile.setValueFormatter(new TimeFormatter('microseconds'))
return profile
}