Import v8 cpu profile (old format) (#177)

As said on #170, i added the support for the old format used by https://github.com/hyj1991/v8-profiler-node8 (which is currently used for pm2.io).
This commit is contained in:
vmarchaud
2018-12-04 12:05:19 -08:00
committed by Jamie Wong
parent c15ca263d3
commit 8cddf3fe81
7 changed files with 217 additions and 8 deletions
@@ -0,0 +1 @@
{"head":{"functionName":"(root)","url":"","lineNumber":-1,"bailoutReason":"","id":1,"scriptId":0,"hitCount":0,"children":[{"functionName":"","url":"","lineNumber":0,"callUID":1,"bailoutReason":"","id":2,"scriptId":0,"hitCount":0,"children":[{"functionName":"a","url":"","lineNumber":0,"callUID":2,"bailoutReason":"","id":3,"scriptId":0,"hitCount":0,"children":[{"functionName":"b","url":"","lineNumber":5,"callUID":3,"bailoutReason":"","id":4,"scriptId":0,"hitCount":0,"children":[{"functionName":"d","url":"","lineNumber":13,"callUID":4,"bailoutReason":"","id":5,"scriptId":0,"hitCount":14,"children":[]}]},{"functionName":"c","url":"","lineNumber":9,"callUID":3,"bailoutReason":"","id":6,"scriptId":0,"hitCount":0,"children":[{"functionName":"d","url":"","lineNumber":13,"callUID":6,"bailoutReason":"","id":7,"scriptId":0,"hitCount":14,"children":[]}]}]}]}]},"startTime":163140,"endTime":163140,"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],"timestamps":[163140599286,163140610861,163140611921,163140612966,163140614236,163140615507,163140616694,163140617968,163140619238,163140620258,163140621507,163140622765,163140624037,163140625303,163140626378,163140627649,163140628923,163140630191,163140631457,163140632746,163140634032,163140635304,163140636440,163140637716,163140638990,163140640255,163140641520,163140642791,163140644063,163140645206]}
@@ -0,0 +1,60 @@
/*
* This script is used to reconstruct an cpu profile from chrome with an old format
* still used in nodejs with v8-profiler
*/
const fs = require('fs')
const data = JSON.parse(fs.readFileSync('../../profiles/Chrome/65/simple.cpuprofile').toString())
const _convertTimeDeltas = (profile) => {
if (!profile.timeDeltas) return null
let lastTimeUsec = profile.startTime
const timestamps = new Array(profile.timeDeltas.length + 1)
for (let i = 0; i < profile.timeDeltas.length; ++i) {
timestamps[i] = lastTimeUsec
lastTimeUsec += profile.timeDeltas[i]
}
timestamps[profile.timeDeltas.length] = lastTimeUsec
return timestamps
}
const reformatNode = node => {
if (!node.children) node.children = []
node.children = node.children.map(childID => {
if (typeof childID !== 'number') return childID
const childNode = data.nodes.find(node => node.id === childID)
if (typeof childNode !== 'object') return null
childNode.callUID = node.id
return childNode
})
return {
functionName: node.callFrame.functionName,
url: node.callFrame.url,
lineNumber: node.callFrame.lineNumber,
callUID: node.callUID,
bailoutReason: '',
id: node.id,
scriptId: 0,
hitCount: node.hitCount,
children: node.children.map(reformatNode)
}
}
// reformat then only keep the root as top level node
const nodes = data.nodes
.map(reformatNode)
.filter(node => node.functionName === '(root)')[0]
// since it can be undefined, create an array so execution still works
if (!data.timeDeltas) {
data.timeDeltas = []
}
fs.writeFileSync('./new.cpuprofile', JSON.stringify({
head: nodes,
startTime: Math.floor(data.startTime / 1000000),
endTime: Math.floor(data.endTime / 1000000),
samples: data.samples,
timestamps: _convertTimeDeltas(data)
}))
@@ -1357,3 +1357,66 @@ exports[`importFromChromeTimeline Workers Chrome 70: profileGroup.name 1`] = `"w
exports[`importFromChromeTimeline: indexToView 1`] = `0`;
exports[`importFromChromeTimeline: profileGroup.name 1`] = `"simple-timeline.json"`;
exports[`importFromV8Profiler Node 10 1`] = `
Object {
"frames": Array [
Frame {
"col": 0,
"file": "",
"key": "(anonymous)::0:0",
"line": 0,
"name": "(anonymous)",
"selfWeight": 11575,
"totalWeight": 44777,
},
Frame {
"col": 0,
"file": "",
"key": "a::0:0",
"line": 0,
"name": "a",
"selfWeight": 0,
"totalWeight": 33202,
},
Frame {
"col": 0,
"file": "",
"key": "b::5:0",
"line": 5,
"name": "b",
"selfWeight": 0,
"totalWeight": 16788,
},
Frame {
"col": 0,
"file": "",
"key": "d::13:0",
"line": 13,
"name": "d",
"selfWeight": 33202,
"totalWeight": 33202,
},
Frame {
"col": 0,
"file": "",
"key": "c::9:0",
"line": 9,
"name": "c",
"selfWeight": 0,
"totalWeight": 16414,
},
],
"name": "example.cpuprofile",
"stacks": Array [
" 599.29ms",
"(anonymous) 11.57ms",
"(anonymous);a;b;d 16.79ms",
"(anonymous);a;c;d 16.41ms",
],
}
`;
exports[`importFromV8Profiler Node 10: indexToView 1`] = `0`;
exports[`importFromV8Profiler Node 10: profileGroup.name 1`] = `"example.cpuprofile"`;
+4
View File
@@ -12,6 +12,10 @@ test('importFromChromeTimeline Chrome 69', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/69/simple.json')
})
test('importFromV8Profiler Node 10', async () => {
await checkProfileSnapshot('./sample/profiles/node/10.11.0/example.cpuprofile')
})
test('importFromChromeTimeline Workers Chrome 70', async () => {
await checkProfileSnapshot('./sample/profiles/Chrome/70/worker.json')
})
+7 -2
View File
@@ -1,6 +1,7 @@
import {Profile, FrameInfo, CallTreeProfileBuilder, ProfileGroup} from '../lib/profile'
import {getOrInsert, lastOf, sortBy, itForEach} from '../lib/utils'
import {TimeFormatter} from '../lib/value-formatters'
import {chromeTreeToNodes, OldCPUProfile} from './v8cpuFormatter'
// See: https://github.com/v8/v8/blob/master/src/inspector/js_protocol.json
@@ -31,7 +32,7 @@ interface CPUProfileCallFrame {
url: string
}
interface CPUProfileNode {
export interface CPUProfileNode {
callFrame: CPUProfileCallFrame
hitCount: number
id: number
@@ -40,7 +41,7 @@ interface CPUProfileNode {
parent?: CPUProfileNode
}
interface CPUProfile {
export interface CPUProfile {
startTime: number
endTime: number
nodes: CPUProfileNode[]
@@ -312,3 +313,7 @@ export function importFromChromeCPUProfile(chromeProfile: CPUProfile): Profile {
profile.setValueFormatter(new TimeFormatter('microseconds'))
return profile.build()
}
export function importFromOldV8CPUProfile(content: OldCPUProfile): Profile {
return importFromChromeCPUProfile(chromeTreeToNodes(content))
}
+11 -6
View File
@@ -1,7 +1,12 @@
import {Profile, ProfileGroup} from '../lib/profile'
import {FileSystemDirectoryEntry} from './file-system-entry'
import {importFromChromeCPUProfile, importFromChromeTimeline, isChromeTimeline} from './chrome'
import {
importFromChromeCPUProfile,
importFromChromeTimeline,
isChromeTimeline,
importFromOldV8CPUProfile,
} from './chrome'
import {importFromStackprof} from './stackprof'
import {importFromInstrumentsDeepCopy, importFromInstrumentsTrace} from './instruments'
import {importFromBGFlameGraph} from './bg-flamegraph'
@@ -84,9 +89,6 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
if (fileName.endsWith('.speedscope.json')) {
console.log('Importing as speedscope json file')
return importSpeedscopeProfiles(JSON.parse(contents))
} else if (fileName.endsWith('.cpuprofile')) {
console.log('Importing as Chrome CPU Profile')
return toGroup(importFromChromeCPUProfile(JSON.parse(contents)))
} else if (fileName.endsWith('.chrome.json') || /Profile-\d{8}T\d{6}/.exec(fileName)) {
console.log('Importing as Chrome Timeline')
return importFromChromeTimeline(JSON.parse(contents), fileName)
@@ -123,11 +125,14 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
console.log('Importing as Firefox profile')
return toGroup(importFromFirefox(parsed))
} else if (isChromeTimeline(parsed)) {
console.log('Importing as Chrome CPU Profile')
console.log('Importing as Chrome Timeline')
return importFromChromeTimeline(parsed, fileName)
} else if ('nodes' in parsed && 'samples' in parsed && 'timeDeltas' in parsed) {
console.log('Importing as Chrome Timeline')
console.log('Importing as Chrome CPU Profile')
return toGroup(importFromChromeCPUProfile(parsed))
} else if ('head' in parsed && 'samples' in parsed && 'timestamps' in parsed) {
console.log('Importing as Chrome CPU Profile (old format)')
return toGroup(importFromOldV8CPUProfile(parsed))
} else if ('mode' in parsed && 'frames' in parsed) {
console.log('Importing as stackprof profile')
return toGroup(importFromStackprof(parsed))
+71
View File
@@ -0,0 +1,71 @@
import {CPUProfile, CPUProfileNode} from './chrome'
/**
* This importer handles an old format used by the C++ API of V8. This format is still used by v8-profiler-node8.
* There are two differences between the two formats:
* - Nodes are a tree in the old format and a flat array in the new format
* - Weights are timestamps in the old format and deltas in the new format.
*
* For more information, see https://github.com/hyj1991/v8-profiler-node8
*/
interface OldCPUProfileNode {
functionName: string
lineNumber: number
scriptId: string
url: string
hitCount: number
bailoutReason: string
id: number
children: OldCPUProfileNode[]
}
export interface OldCPUProfile {
startTime: number
endTime: number
head: OldCPUProfileNode
samples: number[]
timestamps: number[]
}
function treeToArray(root: OldCPUProfileNode): CPUProfileNode[] {
const nodes: CPUProfileNode[] = []
function visit(node: OldCPUProfileNode) {
nodes.push({
id: node.id,
callFrame: {
columnNumber: 0,
functionName: node.functionName,
lineNumber: node.lineNumber,
scriptId: node.scriptId,
url: node.url,
},
hitCount: node.hitCount,
children: node.children.map(child => child.id),
})
node.children.forEach(visit)
}
visit(root)
return nodes
}
function timestampsToDeltas(timestamps: number[], startTime: number): number[] {
return timestamps.map((timestamp, index) => {
const lastTimestamp = index === 0 ? startTime * 1000000 : timestamps[index - 1]
return timestamp - lastTimestamp
})
}
/**
* Convert the old tree-based format to the new flat-array based format
*/
export function chromeTreeToNodes(content: OldCPUProfile): CPUProfile {
// Note that both startTime and endTime are now in microseconds
return {
samples: content.samples,
startTime: content.startTime * 1000000,
endTime: content.endTime * 1000000,
nodes: treeToArray(content.head),
timeDeltas: timestampsToDeltas(content.timestamps, content.startTime),
}
}