Faster Chrome profile import

This commit is contained in:
Jamie Wong 2018-01-17 11:00:39 -08:00
parent c15f4c4764
commit 88eb8068cb
5 changed files with 110 additions and 53 deletions

View File

@ -69,5 +69,7 @@ export class Flamechart {
this.totalWeight = source.getTotalWeight()
source.forEachCall(openFrame, closeFrame)
if (!isFinite(this.minFrameWidth)) this.minFrameWidth = 1
}
}

View File

@ -1,5 +1,5 @@
import {Profile, TimeFormatter, FrameInfo} from '../profile'
import {getOrInsert} from '../utils'
import {getOrInsert, lastOf} from '../utils'
interface TimelineEvent {
pid: number,
@ -50,6 +50,24 @@ export function importFromChromeTimeline(events: TimelineEvent[]) {
return importFromChromeCPUProfile(chromeProfile)
}
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)
@ -90,60 +108,56 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile) {
timeDeltas.push(elapsed)
}
const callFrameToFrameInfo = new Map<CPUProfileCallFrame, FrameInfo>()
let prevStack: CPUProfileNode[] = []
let lastNonGCStackTop: CPUProfileNode | null = null
let value = 0
for (let i = 0; i < samples.length; i++) {
const timeDelta = timeDeltas[i+1] || 0
const nodeId = samples[i]
let node = nodeById.get(nodeId)
if (!node) continue
let stackTop = nodeById.get(nodeId)
if (!stackTop) continue
const stack: FrameInfo[] = []
// Find lowest common ancestor of the current stack and the previous one
let lca: CPUProfileNode | null = null
if (node.callFrame.functionName === "(garbage collector)") {
// 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
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)
if (!lastNonGCStackTop) {
profile.appendSample(stack, timeDelta)
continue
} else {
node = lastNonGCStackTop
}
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)
}
lastNonGCStackTop = node
prevStack = prevStack.concat(toOpen)
value += timeDelta
}
// TODO(jlfwong): This is silly and slow, but good enough for now
for (; node; node = node.parent) {
if (node.callFrame.functionName === '(root)') continue
if (node.callFrame.functionName === '(idle)') continue
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
}
})
stack.push(frame)
}
stack.reverse()
profile.appendSample(stack, 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'))

View File

@ -288,16 +288,26 @@ export class Profile {
let stack = useAppendOrder ? this.appendOrderStack : this.groupedOrderStack
this.addWeightsToNodes(value, stack)
let node = lastOf(stack)
if (node) {
const last = useAppendOrder ? lastOf(node.children) : node.children.find(c => c.frame === frame)
let prevTop = lastOf(stack)
if (prevTop) {
if (useAppendOrder) {
const delta = value - this.lastValue!
if (delta > 0) {
this.samples.push(prevTop)
this.weights.push(value - this.lastValue!)
}
}
const last = useAppendOrder ? lastOf(prevTop.children) : prevTop.children.find(c => c.frame === frame)
let node: CallTreeNode
if (last && last.frame == frame) {
node = last
} else {
const parent = node
node = new CallTreeNode(frame, node)
parent.children.push(node)
node = new CallTreeNode(frame, prevTop)
prevTop.children.push(node)
}
stack.push(node)
}
}
enterFrame(frameInfo: FrameInfo, value: number) {
@ -317,7 +327,12 @@ export class Profile {
this.addWeightsToNodes(value, stack)
if (useAppendOrder) {
this.appendOrderStack.pop()
const leavingStackTop = this.appendOrderStack.pop()
const delta = value - this.lastValue!
if (delta > 0) {
this.samples.push(leavingStackTop!)
this.weights.push(value - this.lastValue!)
}
} else {
this.groupedOrderStack.pop()
}
@ -325,6 +340,7 @@ export class Profile {
leaveFrame(frameInfo: FrameInfo, value: number) {
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
this.addWeightsToFrames(value)
this._leaveFrame(frame, value, true)
this._leaveFrame(frame, value, false)
@ -334,7 +350,7 @@ export class Profile {
if (frameCount === 1) {
this.framesInStack.delete(frame)
} else {
this.framesInStack.set(frame, frameCount)
this.framesInStack.set(frame, frameCount - 1)
}
this.lastValue = value
}

1
sample/simple.cpuprofile Normal file
View File

@ -0,0 +1 @@
{"nodes":[{"id":1,"callFrame":{"functionName":"(root)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":0,"children":[2]},{"id":2,"callFrame":{"functionName":"","scriptId":"164","url":"","lineNumber":0,"columnNumber":0},"hitCount":0,"children":[3]},{"id":3,"callFrame":{"functionName":"a","scriptId":"164","url":"","lineNumber":0,"columnNumber":10},"hitCount":0,"children":[4,6]},{"id":4,"callFrame":{"functionName":"b","scriptId":"164","url":"","lineNumber":5,"columnNumber":10},"hitCount":0,"children":[5]},{"id":5,"callFrame":{"functionName":"d","scriptId":"164","url":"","lineNumber":13,"columnNumber":10},"hitCount":14,"positionTicks":[{"line":16,"ticks":13},{"line":14,"ticks":1}],"children":[]},{"id":6,"callFrame":{"functionName":"c","scriptId":"164","url":"","lineNumber":9,"columnNumber":10},"hitCount":0,"children":[7]},{"id":7,"callFrame":{"functionName":"d","scriptId":"164","url":"","lineNumber":13,"columnNumber":10},"hitCount":14,"positionTicks":[{"line":16,"ticks":14}],"children":[]}],"startTime":163140599286,"endTime":163140647178,"samples":[2,5,5,5,5,5,5,5,5,5,5,5,5,5,5,7,7,7,7,7,7,7,7,7,7,7,7,7,7],"timeDeltas":[11575,1060,1045,1270,1271,1187,1274,1270,1020,1249,1258,1272,1266,1075,1271,1274,1268,1266,1289,1286,1272,1136,1276,1274,1265,1265,1271,1272,1143]}

24
sample/simple.js Normal file
View File

@ -0,0 +1,24 @@
function a() {
b();
c();
}
function b() {
d();
}
function c() {
d();
}
function d() {
let prod = 1;
for (let i = 1; i < 10000000; i++) {
prod *= i
}
return prod;
}
console.profile('a')
a();
console.profileEnd('a')