Add import of v8 heap allocation profile (#170)

This adds support for importing heap profiles from Chrome: https://developers.google.com/web/tools/chrome-devtools/memory-problems/#allocation-profile
This commit is contained in:
vmarchaud 2018-10-08 18:15:39 +02:00 committed by Jamie Wong
parent d78d20e005
commit 4b292b2acf
6 changed files with 5003 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import {importFromLinuxPerf} from './linux-tools-perf'
import {ProfileDataSource, TextProfileDataSource, MaybeCompressedDataReader} from './utils'
import {importAsPprofProfile} from './pprof'
import {decodeBase64} from '../lib/utils'
import {importFromChromeHeapProfile} from './v8heapalloc'
export async function importProfileGroupFromText(
fileName: string,
@ -103,6 +104,9 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
} else if (fileName.endsWith('.v8log.json')) {
console.log('Importing as --prof-process v8 log')
return toGroup(importFromV8ProfLog(JSON.parse(contents)))
} else if (fileName.endsWith('.heapprofile')) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
}
// Second pass: Try to guess what file format it is based on structure
@ -129,6 +133,9 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
console.log('Importing as --prof-process v8 log')
return toGroup(importFromV8ProfLog(parsed))
} else if ('head' in parsed && 'selfSize' in parsed['head']) {
console.log('Importing as Chrome Heap Profile')
return toGroup(importFromChromeHeapProfile(JSON.parse(contents)))
}
} else {
// Format is not JSON

View File

@ -0,0 +1,9 @@
import {checkProfileSnapshot} from '../lib/test-utils'
test('importV8HeapAlloc from Chrome', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/Heap-20181005T144546.heapprofile')
})
test('importV8HeapAlloc from NodeJS', async () => {
await checkProfileSnapshot('./sample/profiles/node/10.11.0/Heap-20181003T105432.heapprofile')
})

109
src/import/v8heapalloc.ts Normal file
View File

@ -0,0 +1,109 @@
import {Profile, FrameInfo, StackListProfileBuilder} from '../lib/profile'
import {getOrInsert} from '../lib/utils'
import {ByteFormatter} from '../lib/value-formatters'
/**
* The V8 Heap Allocation profile is a way to represent heap allocation for each
* javascript function. The format is a simple tree where the weight of each node
* represent the memory allocated by the function and all its callee.
* You can find more information on how to get a profile there :
* https://developers.google.com/web/tools/chrome-devtools/memory-problems/#allocation-profile
* You need to scroll down to "Investigate memory allocation by function"
*
* Note that Node.JS can retrieve this kind of profile via the Inspector protocol.
*/
interface HeapProfileCallFrame {
columnNumber: number
functionName: string
lineNumber: number
scriptId: string
url: string
}
interface HeapProfileNode {
callFrame: HeapProfileCallFrame
selfSize: number
children: HeapProfileNode[]
id: number
parent?: number
totalSize: number
}
interface HeapProfile {
head: HeapProfileNode
}
const callFrameToFrameInfo = new Map<HeapProfileCallFrame, FrameInfo>()
function frameInfoForCallFrame(callFrame: HeapProfileCallFrame) {
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 importFromChromeHeapProfile(chromeProfile: HeapProfile): Profile {
const nodeById = new Map<number, HeapProfileNode>()
let currentId = 0
const computeId = (node: HeapProfileNode, parent?: HeapProfileNode) => {
node.id = currentId++
nodeById.set(node.id, node)
if (parent) {
node.parent = parent.id
}
node.children.forEach(children => computeId(children, node))
}
computeId(chromeProfile.head)
// Compute the total size
const computeTotalSize = (node: HeapProfileNode): number => {
if (node.children.length === 0) return node.selfSize || 0
const totalChild = node.children.reduce((total: number, children) => {
total += computeTotalSize(children)
return total
}, node.selfSize)
node.totalSize = totalChild
return totalChild
}
const total = computeTotalSize(chromeProfile.head)
// Compute all stacks by taking each last node and going upward
const stacks: HeapProfileNode[][] = []
for (let currentNode of nodeById.values()) {
let stack: HeapProfileNode[] = []
stack.push(currentNode)
// While we found a parent
while (true) {
if (currentNode.parent === undefined) break
const parent = nodeById.get(currentNode.parent)
if (parent === undefined) break
// Push the parent at the beginning of the stack
stack.unshift(parent)
currentNode = parent
}
stacks.push(stack)
}
const profile = new StackListProfileBuilder(total)
for (let stack of stacks) {
const lastFrame = stack[stack.length - 1]
profile.appendSampleWithWeight(
stack.map(frame => frameInfoForCallFrame(frame.callFrame)),
lastFrame.selfSize,
)
}
profile.setValueFormatter(new ByteFormatter())
return profile.build()
}