-
Notifications
You must be signed in to change notification settings - Fork 1
/
flame.js
144 lines (139 loc) · 5.11 KB
/
flame.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// source https://github.com/jantimon/cpuprofile-to-flamegraph
/**
* Convert a cpuprofile into a FlameGraph
*/
function convertToMergedFlameGraph(cpuProfile) {
const nodes = convertToTimedFlameGraph(cpuProfile)
// Add all parent nodes
const parentNodes = nodes.map((node) => {
const executionTime = node.value
node = Object.assign({}, node, { children: [], executionTime })
node.selfTime = node.value
while (node.parent && node.parent.children) {
const newParent = Object.assign({}, node.parent, {
children: [node],
executionTime,
})
node.parent = newParent
node = newParent
}
return node
})
const mergedNodes = []
let currentNode = parentNodes[0]
// Merge equal parent nodes
for (let nodeIndex = 1; nodeIndex <= parentNodes.length; nodeIndex++) {
const nextNode = parentNodes[nodeIndex]
const isMergeAble = nextNode !== undefined &&
currentNode.profileNode === nextNode.profileNode &&
currentNode.children.length &&
nextNode.children.length
if (!isMergeAble) {
mergedNodes.push(currentNode)
currentNode = nextNode
}
else {
// Find common child
let currentMergeNode = currentNode
let nextMergeNode = nextNode
while (true) {
// Child nodes are sorted in chronological order
// as nextNode is executed after currentNode it
// is only possible to merge into the last child
const lastChildIndex = currentMergeNode.children.length - 1
const mergeCandidate1 = currentMergeNode.children[lastChildIndex]
const mergeCandidate2 = nextMergeNode.children[0]
// As `getReducedSamples` already reduced all children
// only nodes with children are possible merge targets
const nodesHaveChildren = mergeCandidate1.children.length &&
mergeCandidate2.children.length
if (nodesHaveChildren && mergeCandidate1.profileNode.id === mergeCandidate2.profileNode.id) {
currentMergeNode = mergeCandidate1
nextMergeNode = mergeCandidate2
} else {
break
}
}
// Merge the last mergeable node
currentMergeNode.children.push(nextMergeNode.children[0])
nextMergeNode.children[0].parent = currentMergeNode
const additionalExecutionTime = nextMergeNode.executionTime
let currentExecutionTimeNode = currentMergeNode
while (currentExecutionTimeNode) {
currentExecutionTimeNode.executionTime += additionalExecutionTime
currentExecutionTimeNode = currentExecutionTimeNode.parent
}
}
}
return mergedNodes[0]
}
function convertToTimedFlameGraph(cpuProfile) {
// Convert into FrameGraphNodes structure
const linkedNodes = cpuProfile.nodes.map((node) => ({
name: node.callFrame.functionName || '(anonymous function)',
value: 0,
executionTime: 0,
children: [],
profileNode: node,
nodeModule: node.callFrame.url ? getNodeModuleName(node.callFrame.url) : undefined,
}))
// Create a map for id lookups
const flameGraphNodeById = new Map()
cpuProfile.nodes.forEach((node, i) => {
flameGraphNodeById.set(node.id, linkedNodes[i])
})
// Create reference to children
linkedNodes.forEach((linkedNode) => {
const children = linkedNode.profileNode.children || []
linkedNode.children = children.map((childNodeId) => flameGraphNodeById.get(childNodeId))
linkedNode.children.forEach((child) => { child.parent = linkedNode })
})
const _a = getReducedSamples(cpuProfile)
const { reducedSamples, reducedTimeDeltas } = _a
const timedRootNodes = reducedSamples.map((sampleId, i) => Object.assign({}, flameGraphNodeById.get(sampleId), {
value: reducedTimeDeltas[i],
}))
return timedRootNodes
}
/**
* If multiple samples in a row are the same they can be
* combined
*
* This function returns a merged version of a cpuProfiles
* samples and timeDeltas
*/
function getReducedSamples(_a) {
const { samples, timeDeltas } = _a
const sampleCount = samples.length
const reducedSamples = []
const reducedTimeDeltas = []
if (sampleCount === 0) {
return { reducedSamples, reducedTimeDeltas }
}
let reducedSampleId = samples[0]
let reducedTimeDelta = timeDeltas[0]
for (let i = 1; i <= sampleCount; i++) {
if (reducedSampleId === samples[i]) {
reducedTimeDelta += timeDeltas[i]
} else {
reducedSamples.push(reducedSampleId)
reducedTimeDeltas.push(reducedTimeDelta)
reducedSampleId = samples[i]
reducedTimeDelta = timeDeltas[i]
}
}
return { reducedSamples, reducedTimeDeltas }
}
/**
* Extract the node_modules name from a url
*/
function getNodeModuleName(url) {
const nodeModules = '/node_modules/'
const nodeModulesPosition = url.lastIndexOf(nodeModules)
if (nodeModulesPosition === -1) return undefined
const folderNamePosition = url.indexOf('/', nodeModulesPosition + 1)
const folderNamePositionEnd = url.indexOf('/', folderNamePosition + 1)
if (folderNamePosition === -1 || folderNamePositionEnd === -1) return undefined
return url.substr(folderNamePosition + 1, folderNamePositionEnd - folderNamePosition - 1)
}
module.exports = { convertToMergedFlameGraph }