【转】解读 Nodejs 性能 API:Performance Timing

转载自大专栏

《解读 Nodejs 性能 API:Performance Timing》

前言

本周末,我花了一点时间去查看与学习 Nodejs v8.5.0 更新的内容,其中一点就包括了与性能时间相关的 API,看起来感觉还是比较有趣的。

在 Nodejs v8.5.0 里新增了与性能时间相关的 API:Performance Timing。它提供了 W3C Performance Timeline 规范的实现,该 API 目的是支持高精度性能指标的收集,保存和获取性能相关的度量数据。

Performance Timing 是基于 libuv 内的高精度时间模块 uv_hrtime 来实现。

image

下面简单粗糙的介绍 Performance Timing

Performance

Performance Timing 被放置到 perf_hooks 模块里:

const {
    performance,
    PerformanceObserver
} = require('perf_hooks');

Performance 对象的作用是提供对 “性能时间表” 的访问,这是由 Node.js 进程和用户代码维护的。
在内部,性能时间表只不过是一个 PerformanceEntry 对象的数组,通常它们是按照顺序来存储。

PerformanceEntry 对象最基本的结构包括了:

PerformanceEntry {
    duration: 217.711151,         
    startTime: 1944738.703347,    // 开始记录时的时间戳
    entryType: 'function',        // 类型
    name: 'myFunction'            // 名称
}

PerformanceEntry 可以指定多种类型,目前仅支持:nodeframemarkmeasuregc,和 function

Nodejs 进程时间

当 Nodejs 应用启动时,Nodejs 会默认添加几个 PerformanceEntry 实例到 “性能时间表” 里。
而第一个 PerformanceEntry 实例是用于记录 Nodejs 进程启动时的性能参数。

可以通过 perf_hooks.performance.nodeTiming 来访问它。

目前可以看到的属性包括了:

> perf_hooks.performance.nodeFrame
PerformanceNodeTiming {
    duration: 4512.380027,                          // 进程已激活的毫秒数
    startTime: 158745518.63114,
    entryType: 'node',
    name: 'node',
    arguments: 158745518.756349,                    // 命令行参数处理完成的时间戳
    initialize: 158745519.09161,                    // Nodejs 完成初始化的时间戳
    inspectorStart: 158745522.408488,               // Nodejs 检查器启动完成的时间戳
    loopStart: 158745613.442409,                    // Nodejs 事件循环开始的时间戳
    loopExit: 0,                                    // Nodejs 事件循环退出的时间戳
    loopFrame: 158749857.025862,                    // Nodejs 事件循环的当前迭代开始的时间戳
    bootstrapComplete: 158745613.439273,            // Nodejs 引导过程完成的时间戳。
    third_party_main_start: 0,                      // third party main 处理开始的时间戳 [没有时为 0]
    third_party_main_end: 0,                        // third party main 处理开始的时间戳
    cluster_setup_start: 0,                         // 集群设置启动的时间戳
    cluster_setup_end: 0,                           // 集群设置结束的时间戳
    module_load_start: 158745583.850295,            // 主模块加载开始的时间戳
    module_load_end: 158745583.851643,              // 主模块加载结束的时间戳
    preload_modules_load_start: 158745583.852331,   // 启动预加载模块加载的时间戳
    preload_modules_load_end: 158745583.879369      // 预加载模块加载结束的时间戳
}

事件循环时序

当 Nodejs 应用启动时,Nodejs 自动添加到 “性能时间表” 的第二个实例是用于记录 Nodejs 事件循环的时序。

可以通过 perf_hooks.performance.nodeFrame 来访问它。

目前可以看到的属性包括了:

> perf_hooks.performance.nodeFrame
PerformanceFrame {
    countPerSecond: 9.91151849696801,  // 每秒事件循环迭代次数
    count: 68,                         // 事件循环迭代的总数
    prior: 0.124875,                   // 事件循环的上一次迭代所用的总毫秒数
    entryType: 'frame',
    name: 'frame',
    duration: 128.827398,              // 当前事件循环持续时间
    startTime: 32623025.740256         // 事件循环的当前迭代开始的时间戳
}

用标志来测量

我们知道,可以使用 setTimeout 来做一些延迟操作,比如在一秒后执行某个函数:setTimeout(myFunction, 1000)

但,事实上真的是 1000ms 后执行吗?

使用 performance 提供的 mark,可以轻易的计算出时间差

const {
    performance,
} = require('perf_hooks');

performance.mark('A');          // 标志一下

setTimeout(() => {
    performance.mark('B');      // 再标记一下

    // performance.measure(name, startMark, endMark): 在性能时间表里添加一项
    performance.measure('A to B', 'A', 'B');

    // PerformanceEntry 对象
    const entry = performance.getEntriesByName('A to B')[0];

    console.log(entry);
    /*
        输出结果:
        PerformanceEntry {
            duration: 1002.693559,
            startTime: 4259805.238914,
            entryType: 'measure',
            name: 'A to B'
        }
    */
}, 1000);

你会发现,它多出了 2ms,哇哇坑坑的。

测量函数的执行时间

还可以使用 PerformanceObserver 订阅 'function' 类型。每次调用包装函数时,时序数据将自动添加到 “性能时间表”。

下面通过简单的示例来测量函数的执行时间。

const {
    performance,
    PerformanceObserver
} = require('perf_hooks');

let result = [];

function (n) {
    if (n == 1 || n == 2) {
        return 1;
    }
    return fib(n - 1) + fib(n - 2);
}

// timerify: 在一个新函数中包装一个函数,用于测量包装函数的运行时间
const myfib = performance.timerify(fib);

// 有对象添加到性能时间表时,发出通知
const obs = new PerformanceObserver((list, observer) => {
    // PerformanceEntry 对象列表
    const entries = list.getEntries();

    entries.forEach((entry, index) => {
        // entry[0] 是第一个参数的值,如此类推
        console.log(`${entry.name}(${entry[0]}) = ${result[index]}, run time:`, entry.duration, 'ms');
    });

    // 断开 PerformanceObserver 实例与所有通知的连接。
    obs.disconnect();
    // 从性能时间表中清除所有对象
    performance.clearFunctions();
});

// buffered 为 true 表示异步通知,默认是同步通知
obs.observe({ entryTypes: ['function'], buffered: true });

result.push(
    myfib(5),
    myfib(10),
    myfib(20),
    myfib(30),
);

/*
    输出结果:
    fib(5) = 5, run time: 0.001568 ms
    fib(10) = 55, run time: 0.005568 ms
    fib(20) = 6765, run time: 2.007623 ms
    fib(30) = 832040, run time: 7.03683 ms
*/

可以看到,进度到达了微秒级别。

测量模块加载时间

还可以使用一种特别有趣的方式是测量 Nodejs 应用中为每个模块依赖关系加载时间:

const {
    performance,
    PerformanceObserver
} = require('perf_hooks');

const mod = require('module');

// 依赖模块
mod.Module.prototype.require =
performance.timerify(mod.Module.prototype.require);

require = performance.timerify(require);

const obs = new PerformanceObserver((list, observer) => {
    const entries = list.getEntries();

    entries.forEach((entry) => {
        console.log(`require('${entry[0]}')`, entry.duration, 'ms');
    });

    obs.disconnect();
    performance.clearFunctions();
});

obs.observe({ entryTypes: ['function'], buffered: true });

const fetch = require('node-fetch');

/*
    输出结果:
    require('node-fetch') 32.271811 ms
    require('url') 0.006979 ms
    require('url') 0.005748 ms
    require('http') 6.021816 ms
    require('https') 8.655848 ms
    require('zlib') 1.569498 ms
    require('stream') 0.00739 ms
    require('./lib/body') 11.224192 ms
    require('encoding') 5.052119 ms
    require('iconv-lite') 3.416933 ms
    require('buffer') 0.005747 ms
    require('./bom-handling') 0.550125 ms
    require('./streams') 0.641265 ms
    require('buffer') 0.004927 ms
    require('stream') 0.002463 ms
    require('./extend-node') 0.714342 ms
    require('buffer') 0.004927 ms

    ... 省略 n 个
*/

测量 GC 活动的情况

使用 entryTypes: ['gc'] 可以测量 GC 活动的情况。
observe 后,每次 GC 活动时,都会添加到性能时间表里。

const {
    performance,
    PerformanceObserver
} = require('perf_hooks');


const obs = new PerformanceObserver((list) => {
    console.log(list.getEntries());
    performance.clearGC();

    /*
        输出结果:
        [
            PerformanceEntry {
                duration: 0.89498,         // gc 持续了 0.89498 ms
                startTime: 6039754.909044,
                entryType: 'gc',
                name: 'gc',
                kind: 1
            }
        ]
    */
});

obs.observe({ entryTypes: ['gc'] });

细节可能会改变

API Performance Timing 是 v8.5.0 新增的,目前稳定性为 1,具体的细节可能会改变,敬请留意。

参考资料

  • https://medium.com/the-node-js-collection/timing-is-everything-6d43fc9fd416
  • https://nodejs.org/dist/latest-v8.x/docs/api/perf_hooks.html

你可能感兴趣的:(【转】解读 Nodejs 性能 API:Performance Timing)