2018-01-07 03:37:36 +03:00
|
|
|
import {Profile, TimeFormatter, FrameInfo} from '../profile'
|
2018-01-17 09:36:06 +03:00
|
|
|
import {getOrInsert} from '../utils'
|
2017-12-31 06:48:30 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-01-17 09:36:06 +03:00
|
|
|
interface CPUProfileCallFrame {
|
|
|
|
columnNumber: number,
|
|
|
|
functionName: string,
|
|
|
|
lineNumber: number,
|
|
|
|
scriptId: string,
|
|
|
|
url: string
|
|
|
|
}
|
|
|
|
|
2017-12-31 06:48:30 +03:00
|
|
|
interface CPUProfileNode {
|
2018-01-17 09:36:06 +03:00
|
|
|
callFrame: CPUProfileCallFrame
|
2017-12-31 06:48:30 +03:00
|
|
|
hitCount: number
|
|
|
|
id: number
|
|
|
|
children?: number[]
|
|
|
|
positionTicks?: PositionTickInfo[]
|
|
|
|
parent?: CPUProfileNode
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CPUProfile {
|
|
|
|
startTime: number,
|
|
|
|
endTime: number,
|
|
|
|
nodes: CPUProfileNode[],
|
|
|
|
samples: number[],
|
|
|
|
timeDeltas: number[]
|
|
|
|
}
|
|
|
|
|
2018-01-08 06:50:27 +03:00
|
|
|
export function importFromChromeTimeline(events: TimelineEvent[]) {
|
2017-12-31 06:48:30 +03:00
|
|
|
const profileEvent = events[events.length - 1]
|
|
|
|
const chromeProfile = profileEvent.args.data.cpuProfile as CPUProfile
|
2018-01-08 06:50:27 +03:00
|
|
|
return importFromChromeCPUProfile(chromeProfile)
|
|
|
|
}
|
|
|
|
|
|
|
|
export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
|
2017-12-31 06:48:30 +03:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-01-17 09:36:06 +03:00
|
|
|
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
|
|
|
|
|
2018-01-09 09:38:01 +03:00
|
|
|
let lastNonGCStackTop: CPUProfileNode | null = null
|
2017-12-31 06:48:30 +03:00
|
|
|
for (let i = 0; i < samples.length; i++) {
|
2017-12-31 10:13:57 +03:00
|
|
|
const timeDelta = timeDeltas[i+1] || 0
|
2017-12-31 06:48:30 +03:00
|
|
|
const nodeId = samples[i]
|
|
|
|
let node = nodeById.get(nodeId)
|
|
|
|
if (!node) continue
|
|
|
|
|
|
|
|
const stack: FrameInfo[] = []
|
2018-01-09 09:38:01 +03:00
|
|
|
|
|
|
|
if (node.callFrame.functionName === "(garbage collector)") {
|
|
|
|
// Place GC calls on top of the previous call stack
|
2018-01-17 09:36:06 +03:00
|
|
|
const frame = getOrInsert(callFrameToFrameInfo, node.callFrame, (callFrame) => ({
|
|
|
|
key: callFrame.functionName,
|
|
|
|
name: callFrame.functionName,
|
|
|
|
file: callFrame.url,
|
|
|
|
line: callFrame.lineNumber,
|
|
|
|
col: callFrame.columnNumber
|
|
|
|
}))
|
|
|
|
stack.push(frame)
|
2018-01-09 09:38:01 +03:00
|
|
|
if (!lastNonGCStackTop) {
|
|
|
|
profile.appendSample(stack, timeDelta)
|
|
|
|
continue
|
|
|
|
} else {
|
|
|
|
node = lastNonGCStackTop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
lastNonGCStackTop = node
|
|
|
|
|
|
|
|
// TODO(jlfwong): This is silly and slow, but good enough for now
|
|
|
|
for (; node; node = node.parent) {
|
2018-01-01 03:51:26 +03:00
|
|
|
if (node.callFrame.functionName === '(root)') continue
|
|
|
|
if (node.callFrame.functionName === '(idle)') continue
|
2018-01-08 05:36:23 +03:00
|
|
|
|
2018-01-17 09:36:06 +03:00
|
|
|
const frame = getOrInsert(callFrameToFrameInfo, node.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
|
|
|
|
}
|
2017-12-31 06:48:30 +03:00
|
|
|
})
|
2018-01-17 09:36:06 +03:00
|
|
|
stack.push(frame)
|
2017-12-31 06:48:30 +03:00
|
|
|
}
|
|
|
|
stack.reverse()
|
|
|
|
|
|
|
|
profile.appendSample(stack, timeDelta)
|
|
|
|
}
|
2018-01-07 03:37:36 +03:00
|
|
|
|
2018-01-07 04:04:54 +03:00
|
|
|
profile.setValueFormatter(new TimeFormatter('microseconds'))
|
2017-12-31 06:48:30 +03:00
|
|
|
return profile
|
|
|
|
}
|