了解javascript cpu配置文件

时间:2019-01-01 23:02:09

标签: javascript google-chrome profiling cpu

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渲染为火焰图:

FlameGraph Example

不过,使用Google Chrome DevTools加载相同的json文件还可以查看执行时间,甚至可以查看不同调用之间的暂停时间:

ChromeDevTool Profile Example

是否可以渲染类似Google Chrome开发者工具的cpu配置文件图表?

1 个答案:

答案 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
    );
}