Google Chrome浏览器以及the NodeJs inspector允许生成具有以下JSON结构的cpu配置文件:
摘录
{
"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]
}
从文档中:https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-Profile
nodes
array ProfileNode-配置文件节点列表。第一项是根节点。
startTime
号-分析开始时间戳(以微秒为单位)。
endTime
号-分析结束时间戳(以微秒为单位)。
samples
数组[整数]-样本顶部节点的ID。
timeDeltas
array [整数]-相邻样本之间的时间间隔,以微秒为单位。第一个增量是相对于配置文件startTime的。
有d3-flame-graph之类的多个库,它们可以将所有nodes
渲染为火焰图:
不过,使用Google Chrome DevTools加载相同的json文件还可以查看执行时间,甚至可以查看不同调用之间的暂停时间:
是否可以渲染类似Google Chrome开发者工具的cpu配置文件图表?
答案 0 :(得分:0)
对于samples
中的每个配置文件ID,timeDeltas
中还有一个微秒的测量值。
将samples
内的ID与nodes
内的条目结合在一起,使我可以获得所需的所有信息。
此后,可以将nodes
的所有父代加起来并计算执行时间。
最后,将所有相等的父代合并在一起,以更快地绘制图表。
您可以看一下也在github和npm上发布的代码:
代码:
/**
* 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
);
}