转载自大专栏
《解读 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
来实现。
下面简单粗糙的介绍 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 可以指定多种类型,目前仅支持:node
,frame
,mark
,measure
,gc
,和 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