【问题标题】:Understanding javascript cpu profiles了解 javascript cpu 配置文件
【发布时间】:2019-01-01 23:02:08
【问题描述】:

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 number - 分析开始时间戳(以微秒为单位)。
endTime number - 分析结束时间戳(以微秒为单位)。
samples array [ integer ] - 样本 ID 顶部节点。
timeDeltas array [ integer ] - 相邻样本之间的时间间隔(以微秒为单位)。第一个增量是相对于配置文件 startTime。

有多个库,如 d3-flame-graph,允许将所有 nodes 渲染为火焰图:

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

有没有办法像谷歌浏览器开发工具一样渲染​​一个类似的 cpu 配置图表?

【问题讨论】:

    标签: javascript google-chrome profiling cpu


    【解决方案1】:

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

    【讨论】:

      猜你喜欢
      • 2014-11-02
      • 2010-11-30
      • 2023-03-10
      • 2011-10-19
      • 1970-01-01
      • 1970-01-01
      • 2023-03-04
      • 1970-01-01
      • 2022-01-27
      相关资源
      最近更新 更多