Understanding javascript cpu profiles
Google Chrome and also the NodeJs inspector allow to generate a cpu profile with the following JSON structure:
excerpt
{
"nodes": [
{
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"children": [2, 71],
"hitCount": 0,
"id": 1
}
],
"startTime": 194737272346,
"endTime": 194737292265,
"samples": [1, 1, 1],
"timeDeltas": [7489, 1185, 1271]
}
From the docs: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
nodes
array ProfileNode - The list of profile nodes. First item is the root node.startTime
number - Profiling start timestamp in microseconds.endTime
number - Profiling end timestamp in microseconds.samples
array [ integer ] - Ids of samples top nodes.timeDeltas
array [ integer ] - Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime.
There are multiple libraries like d3-flame-graph which allow to render all nodes
as a flame graph:
However loading the same json file using the Google Chrome DevTools allows also to see the execution time and even the pauses between different calls:
Is there a way to render render a similar cpu profile chart like the Google Chrome Dev Tools?
javascript google-chrome profiling cpu
add a comment |
Google Chrome and also the NodeJs inspector allow to generate a cpu profile with the following JSON structure:
excerpt
{
"nodes": [
{
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"children": [2, 71],
"hitCount": 0,
"id": 1
}
],
"startTime": 194737272346,
"endTime": 194737292265,
"samples": [1, 1, 1],
"timeDeltas": [7489, 1185, 1271]
}
From the docs: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
nodes
array ProfileNode - The list of profile nodes. First item is the root node.startTime
number - Profiling start timestamp in microseconds.endTime
number - Profiling end timestamp in microseconds.samples
array [ integer ] - Ids of samples top nodes.timeDeltas
array [ integer ] - Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime.
There are multiple libraries like d3-flame-graph which allow to render all nodes
as a flame graph:
However loading the same json file using the Google Chrome DevTools allows also to see the execution time and even the pauses between different calls:
Is there a way to render render a similar cpu profile chart like the Google Chrome Dev Tools?
javascript google-chrome profiling cpu
add a comment |
Google Chrome and also the NodeJs inspector allow to generate a cpu profile with the following JSON structure:
excerpt
{
"nodes": [
{
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"children": [2, 71],
"hitCount": 0,
"id": 1
}
],
"startTime": 194737272346,
"endTime": 194737292265,
"samples": [1, 1, 1],
"timeDeltas": [7489, 1185, 1271]
}
From the docs: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
nodes
array ProfileNode - The list of profile nodes. First item is the root node.startTime
number - Profiling start timestamp in microseconds.endTime
number - Profiling end timestamp in microseconds.samples
array [ integer ] - Ids of samples top nodes.timeDeltas
array [ integer ] - Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime.
There are multiple libraries like d3-flame-graph which allow to render all nodes
as a flame graph:
However loading the same json file using the Google Chrome DevTools allows also to see the execution time and even the pauses between different calls:
Is there a way to render render a similar cpu profile chart like the Google Chrome Dev Tools?
javascript google-chrome profiling cpu
Google Chrome and also the NodeJs inspector allow to generate a cpu profile with the following JSON structure:
excerpt
{
"nodes": [
{
"callFrame": {
"functionName": "(root)",
"scriptId": "0",
"url": "",
"lineNumber": -1,
"columnNumber": -1
},
"children": [2, 71],
"hitCount": 0,
"id": 1
}
],
"startTime": 194737272346,
"endTime": 194737292265,
"samples": [1, 1, 1],
"timeDeltas": [7489, 1185, 1271]
}
From the docs: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
nodes
array ProfileNode - The list of profile nodes. First item is the root node.startTime
number - Profiling start timestamp in microseconds.endTime
number - Profiling end timestamp in microseconds.samples
array [ integer ] - Ids of samples top nodes.timeDeltas
array [ integer ] - Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime.
There are multiple libraries like d3-flame-graph which allow to render all nodes
as a flame graph:
However loading the same json file using the Google Chrome DevTools allows also to see the execution time and even the pauses between different calls:
Is there a way to render render a similar cpu profile chart like the Google Chrome Dev Tools?
javascript google-chrome profiling cpu
javascript google-chrome profiling cpu
edited Jan 2 at 9:45
jantimon
asked Jan 1 at 23:02
jantimonjantimon
21.2k2196161
21.2k2196161
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
For every profile id in samples
there is also a microsecond measurement in timeDeltas
.
Combining the ids inside samples
with the entries inside nodes
allowed me to get all information needed.
After that it is possible to add up all parents of the nodes
and to calculate the execution time.
In the end all equal parents are merged together for faster chart renderings.
You can take a look at the code which is also released on github and npm:
- https://github.com/jantimon/cpuprofile-to-flamegraph
- https://www.npmjs.com/package/cpuprofile-to-flamegraph
Code:
/**
* A parsed .cpuprofile which can be generated from
* chrome or https://nodejs.org/api/inspector.html#inspector_cpu_profiler
*
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
*/
export type Profile = {
/**
* The list of profile nodes. First item is the root node.
*/
nodes: Array<ProfileNode>;
/**
* Profiling start timestamp in microseconds.
*/
startTime: number;
/**
* Profiling end timestamp in microseconds.
*/
endTime: number;
/**
* Ids of samples top nodes.
*/
samples: Array<number>;
/**
* Time intervals between adjacent samples in microseconds.
* The first delta is relative to the profile startTime.
*/
timeDeltas: Array<number>;
};
/**
* Profile node. Holds callsite information, execution statistics and child nodes.
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ProfileNode
*/
export type ProfileNode = {
/**
* Unique id of the node.
*/
id: number;
/**
* Runtime.CallFrame
* Function location
*/
callFrame: {
/**
* JavaScript function name.
*/
functionName?: string;
/**
* JavaScript script id.
*/
scriptId: string;
/**
* JavaScript script name or url.
*/
url: string;
/**
* JavaScript script line number (0-based).
*/
lineNumber: number;
/**
* JavaScript script column number (0-based).
*/
columnNumber: number;
};
/**
* Number of samples where this node was on top of the call stack.
*/
hitCount?: number;
/**
* Child node ids.
*/
children?: number;
};
/**
* D3-FlameGraph input format
* https://github.com/spiermar/d3-flame-graph#input-format
*/
export type FlameGraphNode = {
/**
* JavaScript function name.
*/
name: string;
/**
* Self execution time
*/
value: number;
/**
* Execution time including child nodes
*/
executionTime: number;
/**
* Child nodes
*/
children: Array<FlameGraphNode>;
/**
* Original profiler node
*/
profileNode: ProfileNode;
/**
* nodeModule name if known
*/
nodeModule?: string;
/**
* Parent node
*/
parent?: FlameGraphNode;
};
/**
* Convert a cpuprofile into a FlameGraph
*/
export function convertToMergedFlameGraph(cpuProfile: Profile): FlameGraphNode {
const nodes = convertToTimedFlameGraph(cpuProfile);
// Add all parent nodes
const parentNodes = nodes.map(node => {
const executionTime = node.value;
node = Object.assign({}, node, { children: , executionTime });
while (node.parent && node.parent.children) {
const newParent = Object.assign({}, node.parent, {
children: [node],
executionTime
});
node.parent = newParent;
node = newParent;
}
return node;
});
const mergedNodes: Array<FlameGraphNode> = ;
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:
| FlameGraphNode
| undefined = currentMergeNode;
while (currentExecutionTimeNode) {
currentExecutionTimeNode.executionTime += additionalExecutionTime;
currentExecutionTimeNode = currentExecutionTimeNode.parent;
}
}
}
return mergedNodes[0];
}
function convertToTimedFlameGraph(cpuProfile: Profile): Array<FlameGraphNode> {
// Convert into FrameGraphNodes structure
const linkedNodes: Array<FlameGraphNode> = cpuProfile.nodes.map(
(node: ProfileNode) => ({
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<number, FlameGraphNode>();
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) as FlameGraphNode
);
linkedNode.children.forEach(child => {
child.parent = linkedNode;
});
});
const { reducedSamples, reducedTimeDeltas } = getReducedSamples(cpuProfile);
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({
samples,
timeDeltas
}: {
samples: Array<number>;
timeDeltas: Array<number>;
}): { reducedSamples: Array<number>; reducedTimeDeltas: Array<number> } {
const sampleCount = samples.length;
const reducedSamples: Array<number> = ;
const reducedTimeDeltas: Array<number> = ;
if (sampleCount === 0) {
return { reducedSamples, reducedTimeDeltas };
}
let reducedSampleId = samples[0];
let reducedTimeDelta = timeDeltas[0];
for (let i = 0; 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: string): string | undefined {
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
);
}
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53999609%2funderstanding-javascript-cpu-profiles%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
For every profile id in samples
there is also a microsecond measurement in timeDeltas
.
Combining the ids inside samples
with the entries inside nodes
allowed me to get all information needed.
After that it is possible to add up all parents of the nodes
and to calculate the execution time.
In the end all equal parents are merged together for faster chart renderings.
You can take a look at the code which is also released on github and npm:
- https://github.com/jantimon/cpuprofile-to-flamegraph
- https://www.npmjs.com/package/cpuprofile-to-flamegraph
Code:
/**
* A parsed .cpuprofile which can be generated from
* chrome or https://nodejs.org/api/inspector.html#inspector_cpu_profiler
*
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
*/
export type Profile = {
/**
* The list of profile nodes. First item is the root node.
*/
nodes: Array<ProfileNode>;
/**
* Profiling start timestamp in microseconds.
*/
startTime: number;
/**
* Profiling end timestamp in microseconds.
*/
endTime: number;
/**
* Ids of samples top nodes.
*/
samples: Array<number>;
/**
* Time intervals between adjacent samples in microseconds.
* The first delta is relative to the profile startTime.
*/
timeDeltas: Array<number>;
};
/**
* Profile node. Holds callsite information, execution statistics and child nodes.
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ProfileNode
*/
export type ProfileNode = {
/**
* Unique id of the node.
*/
id: number;
/**
* Runtime.CallFrame
* Function location
*/
callFrame: {
/**
* JavaScript function name.
*/
functionName?: string;
/**
* JavaScript script id.
*/
scriptId: string;
/**
* JavaScript script name or url.
*/
url: string;
/**
* JavaScript script line number (0-based).
*/
lineNumber: number;
/**
* JavaScript script column number (0-based).
*/
columnNumber: number;
};
/**
* Number of samples where this node was on top of the call stack.
*/
hitCount?: number;
/**
* Child node ids.
*/
children?: number;
};
/**
* D3-FlameGraph input format
* https://github.com/spiermar/d3-flame-graph#input-format
*/
export type FlameGraphNode = {
/**
* JavaScript function name.
*/
name: string;
/**
* Self execution time
*/
value: number;
/**
* Execution time including child nodes
*/
executionTime: number;
/**
* Child nodes
*/
children: Array<FlameGraphNode>;
/**
* Original profiler node
*/
profileNode: ProfileNode;
/**
* nodeModule name if known
*/
nodeModule?: string;
/**
* Parent node
*/
parent?: FlameGraphNode;
};
/**
* Convert a cpuprofile into a FlameGraph
*/
export function convertToMergedFlameGraph(cpuProfile: Profile): FlameGraphNode {
const nodes = convertToTimedFlameGraph(cpuProfile);
// Add all parent nodes
const parentNodes = nodes.map(node => {
const executionTime = node.value;
node = Object.assign({}, node, { children: , executionTime });
while (node.parent && node.parent.children) {
const newParent = Object.assign({}, node.parent, {
children: [node],
executionTime
});
node.parent = newParent;
node = newParent;
}
return node;
});
const mergedNodes: Array<FlameGraphNode> = ;
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:
| FlameGraphNode
| undefined = currentMergeNode;
while (currentExecutionTimeNode) {
currentExecutionTimeNode.executionTime += additionalExecutionTime;
currentExecutionTimeNode = currentExecutionTimeNode.parent;
}
}
}
return mergedNodes[0];
}
function convertToTimedFlameGraph(cpuProfile: Profile): Array<FlameGraphNode> {
// Convert into FrameGraphNodes structure
const linkedNodes: Array<FlameGraphNode> = cpuProfile.nodes.map(
(node: ProfileNode) => ({
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<number, FlameGraphNode>();
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) as FlameGraphNode
);
linkedNode.children.forEach(child => {
child.parent = linkedNode;
});
});
const { reducedSamples, reducedTimeDeltas } = getReducedSamples(cpuProfile);
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({
samples,
timeDeltas
}: {
samples: Array<number>;
timeDeltas: Array<number>;
}): { reducedSamples: Array<number>; reducedTimeDeltas: Array<number> } {
const sampleCount = samples.length;
const reducedSamples: Array<number> = ;
const reducedTimeDeltas: Array<number> = ;
if (sampleCount === 0) {
return { reducedSamples, reducedTimeDeltas };
}
let reducedSampleId = samples[0];
let reducedTimeDelta = timeDeltas[0];
for (let i = 0; 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: string): string | undefined {
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
);
}
add a comment |
For every profile id in samples
there is also a microsecond measurement in timeDeltas
.
Combining the ids inside samples
with the entries inside nodes
allowed me to get all information needed.
After that it is possible to add up all parents of the nodes
and to calculate the execution time.
In the end all equal parents are merged together for faster chart renderings.
You can take a look at the code which is also released on github and npm:
- https://github.com/jantimon/cpuprofile-to-flamegraph
- https://www.npmjs.com/package/cpuprofile-to-flamegraph
Code:
/**
* A parsed .cpuprofile which can be generated from
* chrome or https://nodejs.org/api/inspector.html#inspector_cpu_profiler
*
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
*/
export type Profile = {
/**
* The list of profile nodes. First item is the root node.
*/
nodes: Array<ProfileNode>;
/**
* Profiling start timestamp in microseconds.
*/
startTime: number;
/**
* Profiling end timestamp in microseconds.
*/
endTime: number;
/**
* Ids of samples top nodes.
*/
samples: Array<number>;
/**
* Time intervals between adjacent samples in microseconds.
* The first delta is relative to the profile startTime.
*/
timeDeltas: Array<number>;
};
/**
* Profile node. Holds callsite information, execution statistics and child nodes.
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ProfileNode
*/
export type ProfileNode = {
/**
* Unique id of the node.
*/
id: number;
/**
* Runtime.CallFrame
* Function location
*/
callFrame: {
/**
* JavaScript function name.
*/
functionName?: string;
/**
* JavaScript script id.
*/
scriptId: string;
/**
* JavaScript script name or url.
*/
url: string;
/**
* JavaScript script line number (0-based).
*/
lineNumber: number;
/**
* JavaScript script column number (0-based).
*/
columnNumber: number;
};
/**
* Number of samples where this node was on top of the call stack.
*/
hitCount?: number;
/**
* Child node ids.
*/
children?: number;
};
/**
* D3-FlameGraph input format
* https://github.com/spiermar/d3-flame-graph#input-format
*/
export type FlameGraphNode = {
/**
* JavaScript function name.
*/
name: string;
/**
* Self execution time
*/
value: number;
/**
* Execution time including child nodes
*/
executionTime: number;
/**
* Child nodes
*/
children: Array<FlameGraphNode>;
/**
* Original profiler node
*/
profileNode: ProfileNode;
/**
* nodeModule name if known
*/
nodeModule?: string;
/**
* Parent node
*/
parent?: FlameGraphNode;
};
/**
* Convert a cpuprofile into a FlameGraph
*/
export function convertToMergedFlameGraph(cpuProfile: Profile): FlameGraphNode {
const nodes = convertToTimedFlameGraph(cpuProfile);
// Add all parent nodes
const parentNodes = nodes.map(node => {
const executionTime = node.value;
node = Object.assign({}, node, { children: , executionTime });
while (node.parent && node.parent.children) {
const newParent = Object.assign({}, node.parent, {
children: [node],
executionTime
});
node.parent = newParent;
node = newParent;
}
return node;
});
const mergedNodes: Array<FlameGraphNode> = ;
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:
| FlameGraphNode
| undefined = currentMergeNode;
while (currentExecutionTimeNode) {
currentExecutionTimeNode.executionTime += additionalExecutionTime;
currentExecutionTimeNode = currentExecutionTimeNode.parent;
}
}
}
return mergedNodes[0];
}
function convertToTimedFlameGraph(cpuProfile: Profile): Array<FlameGraphNode> {
// Convert into FrameGraphNodes structure
const linkedNodes: Array<FlameGraphNode> = cpuProfile.nodes.map(
(node: ProfileNode) => ({
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<number, FlameGraphNode>();
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) as FlameGraphNode
);
linkedNode.children.forEach(child => {
child.parent = linkedNode;
});
});
const { reducedSamples, reducedTimeDeltas } = getReducedSamples(cpuProfile);
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({
samples,
timeDeltas
}: {
samples: Array<number>;
timeDeltas: Array<number>;
}): { reducedSamples: Array<number>; reducedTimeDeltas: Array<number> } {
const sampleCount = samples.length;
const reducedSamples: Array<number> = ;
const reducedTimeDeltas: Array<number> = ;
if (sampleCount === 0) {
return { reducedSamples, reducedTimeDeltas };
}
let reducedSampleId = samples[0];
let reducedTimeDelta = timeDeltas[0];
for (let i = 0; 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: string): string | undefined {
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
);
}
add a comment |
For every profile id in samples
there is also a microsecond measurement in timeDeltas
.
Combining the ids inside samples
with the entries inside nodes
allowed me to get all information needed.
After that it is possible to add up all parents of the nodes
and to calculate the execution time.
In the end all equal parents are merged together for faster chart renderings.
You can take a look at the code which is also released on github and npm:
- https://github.com/jantimon/cpuprofile-to-flamegraph
- https://www.npmjs.com/package/cpuprofile-to-flamegraph
Code:
/**
* A parsed .cpuprofile which can be generated from
* chrome or https://nodejs.org/api/inspector.html#inspector_cpu_profiler
*
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
*/
export type Profile = {
/**
* The list of profile nodes. First item is the root node.
*/
nodes: Array<ProfileNode>;
/**
* Profiling start timestamp in microseconds.
*/
startTime: number;
/**
* Profiling end timestamp in microseconds.
*/
endTime: number;
/**
* Ids of samples top nodes.
*/
samples: Array<number>;
/**
* Time intervals between adjacent samples in microseconds.
* The first delta is relative to the profile startTime.
*/
timeDeltas: Array<number>;
};
/**
* Profile node. Holds callsite information, execution statistics and child nodes.
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ProfileNode
*/
export type ProfileNode = {
/**
* Unique id of the node.
*/
id: number;
/**
* Runtime.CallFrame
* Function location
*/
callFrame: {
/**
* JavaScript function name.
*/
functionName?: string;
/**
* JavaScript script id.
*/
scriptId: string;
/**
* JavaScript script name or url.
*/
url: string;
/**
* JavaScript script line number (0-based).
*/
lineNumber: number;
/**
* JavaScript script column number (0-based).
*/
columnNumber: number;
};
/**
* Number of samples where this node was on top of the call stack.
*/
hitCount?: number;
/**
* Child node ids.
*/
children?: number;
};
/**
* D3-FlameGraph input format
* https://github.com/spiermar/d3-flame-graph#input-format
*/
export type FlameGraphNode = {
/**
* JavaScript function name.
*/
name: string;
/**
* Self execution time
*/
value: number;
/**
* Execution time including child nodes
*/
executionTime: number;
/**
* Child nodes
*/
children: Array<FlameGraphNode>;
/**
* Original profiler node
*/
profileNode: ProfileNode;
/**
* nodeModule name if known
*/
nodeModule?: string;
/**
* Parent node
*/
parent?: FlameGraphNode;
};
/**
* Convert a cpuprofile into a FlameGraph
*/
export function convertToMergedFlameGraph(cpuProfile: Profile): FlameGraphNode {
const nodes = convertToTimedFlameGraph(cpuProfile);
// Add all parent nodes
const parentNodes = nodes.map(node => {
const executionTime = node.value;
node = Object.assign({}, node, { children: , executionTime });
while (node.parent && node.parent.children) {
const newParent = Object.assign({}, node.parent, {
children: [node],
executionTime
});
node.parent = newParent;
node = newParent;
}
return node;
});
const mergedNodes: Array<FlameGraphNode> = ;
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:
| FlameGraphNode
| undefined = currentMergeNode;
while (currentExecutionTimeNode) {
currentExecutionTimeNode.executionTime += additionalExecutionTime;
currentExecutionTimeNode = currentExecutionTimeNode.parent;
}
}
}
return mergedNodes[0];
}
function convertToTimedFlameGraph(cpuProfile: Profile): Array<FlameGraphNode> {
// Convert into FrameGraphNodes structure
const linkedNodes: Array<FlameGraphNode> = cpuProfile.nodes.map(
(node: ProfileNode) => ({
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<number, FlameGraphNode>();
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) as FlameGraphNode
);
linkedNode.children.forEach(child => {
child.parent = linkedNode;
});
});
const { reducedSamples, reducedTimeDeltas } = getReducedSamples(cpuProfile);
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({
samples,
timeDeltas
}: {
samples: Array<number>;
timeDeltas: Array<number>;
}): { reducedSamples: Array<number>; reducedTimeDeltas: Array<number> } {
const sampleCount = samples.length;
const reducedSamples: Array<number> = ;
const reducedTimeDeltas: Array<number> = ;
if (sampleCount === 0) {
return { reducedSamples, reducedTimeDeltas };
}
let reducedSampleId = samples[0];
let reducedTimeDelta = timeDeltas[0];
for (let i = 0; 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: string): string | undefined {
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
);
}
For every profile id in samples
there is also a microsecond measurement in timeDeltas
.
Combining the ids inside samples
with the entries inside nodes
allowed me to get all information needed.
After that it is possible to add up all parents of the nodes
and to calculate the execution time.
In the end all equal parents are merged together for faster chart renderings.
You can take a look at the code which is also released on github and npm:
- https://github.com/jantimon/cpuprofile-to-flamegraph
- https://www.npmjs.com/package/cpuprofile-to-flamegraph
Code:
/**
* A parsed .cpuprofile which can be generated from
* chrome or https://nodejs.org/api/inspector.html#inspector_cpu_profiler
*
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
*/
export type Profile = {
/**
* The list of profile nodes. First item is the root node.
*/
nodes: Array<ProfileNode>;
/**
* Profiling start timestamp in microseconds.
*/
startTime: number;
/**
* Profiling end timestamp in microseconds.
*/
endTime: number;
/**
* Ids of samples top nodes.
*/
samples: Array<number>;
/**
* Time intervals between adjacent samples in microseconds.
* The first delta is relative to the profile startTime.
*/
timeDeltas: Array<number>;
};
/**
* Profile node. Holds callsite information, execution statistics and child nodes.
* https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ProfileNode
*/
export type ProfileNode = {
/**
* Unique id of the node.
*/
id: number;
/**
* Runtime.CallFrame
* Function location
*/
callFrame: {
/**
* JavaScript function name.
*/
functionName?: string;
/**
* JavaScript script id.
*/
scriptId: string;
/**
* JavaScript script name or url.
*/
url: string;
/**
* JavaScript script line number (0-based).
*/
lineNumber: number;
/**
* JavaScript script column number (0-based).
*/
columnNumber: number;
};
/**
* Number of samples where this node was on top of the call stack.
*/
hitCount?: number;
/**
* Child node ids.
*/
children?: number;
};
/**
* D3-FlameGraph input format
* https://github.com/spiermar/d3-flame-graph#input-format
*/
export type FlameGraphNode = {
/**
* JavaScript function name.
*/
name: string;
/**
* Self execution time
*/
value: number;
/**
* Execution time including child nodes
*/
executionTime: number;
/**
* Child nodes
*/
children: Array<FlameGraphNode>;
/**
* Original profiler node
*/
profileNode: ProfileNode;
/**
* nodeModule name if known
*/
nodeModule?: string;
/**
* Parent node
*/
parent?: FlameGraphNode;
};
/**
* Convert a cpuprofile into a FlameGraph
*/
export function convertToMergedFlameGraph(cpuProfile: Profile): FlameGraphNode {
const nodes = convertToTimedFlameGraph(cpuProfile);
// Add all parent nodes
const parentNodes = nodes.map(node => {
const executionTime = node.value;
node = Object.assign({}, node, { children: , executionTime });
while (node.parent && node.parent.children) {
const newParent = Object.assign({}, node.parent, {
children: [node],
executionTime
});
node.parent = newParent;
node = newParent;
}
return node;
});
const mergedNodes: Array<FlameGraphNode> = ;
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:
| FlameGraphNode
| undefined = currentMergeNode;
while (currentExecutionTimeNode) {
currentExecutionTimeNode.executionTime += additionalExecutionTime;
currentExecutionTimeNode = currentExecutionTimeNode.parent;
}
}
}
return mergedNodes[0];
}
function convertToTimedFlameGraph(cpuProfile: Profile): Array<FlameGraphNode> {
// Convert into FrameGraphNodes structure
const linkedNodes: Array<FlameGraphNode> = cpuProfile.nodes.map(
(node: ProfileNode) => ({
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<number, FlameGraphNode>();
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) as FlameGraphNode
);
linkedNode.children.forEach(child => {
child.parent = linkedNode;
});
});
const { reducedSamples, reducedTimeDeltas } = getReducedSamples(cpuProfile);
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({
samples,
timeDeltas
}: {
samples: Array<number>;
timeDeltas: Array<number>;
}): { reducedSamples: Array<number>; reducedTimeDeltas: Array<number> } {
const sampleCount = samples.length;
const reducedSamples: Array<number> = ;
const reducedTimeDeltas: Array<number> = ;
if (sampleCount === 0) {
return { reducedSamples, reducedTimeDeltas };
}
let reducedSampleId = samples[0];
let reducedTimeDelta = timeDeltas[0];
for (let i = 0; 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: string): string | undefined {
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
);
}
answered Jan 3 at 13:40
jantimonjantimon
21.2k2196161
21.2k2196161
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53999609%2funderstanding-javascript-cpu-profiles%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown