mirror of
https://github.com/jlfwong/speedscope.git
synced 2024-12-12 11:17:10 +03:00
170 lines
4.6 KiB
TypeScript
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
|
|
} |