作者James M Snell开源架构师
计时就是一切
长期以来,一直有个问题很想解决,这周末我终于解决了一部分。那就是比照Node.js核心性能测试时间轴接口(Performance Timeline)的细节要求,实现了对其一些基本的支持。希望能很快全部落实。
肯定还有很多地方磕磕绊绊,当然还有很多问题要考虑,不过目前先这样了,所以本文接下来说的内容将来可能会有变动。
require(‘perf_hooks’)函数
新实现的API接口可以通过调用require('perf_hooks')函数来使用。.
const {
performance,
PerformanceObserver
} = require('perf_hooks');
其中,性能(performance)属性返回一个性能(Performance)对象实例,它大致符合高分辨率时间(High Resolution Time)接口的细节要求。
Performance对象的作用是让用户能访问性能时间轴(“Performance Timeline”)。所谓性能时间轴,就是由多个性能重要事件节点(milestone)和测量结果(measures)组成的逻辑序列,这两者由Node.js进程和用户代码共同维持。
从内部来看,时间轴只不过是一个性能条目(PerformanceEntry)对象组成的链表而已。每个PerformanceEntry条目都有名字、类型、起始时间点和时长这几个属性。时间轴中任意一个给定的瞬间,都可能存在多个不同类型PerformanceEntry对象。我完成的部分目前支持四种特定的类型(译注:应该是六种):节点'node',帧'frame',标记'mark',测量结果'measure',垃圾回收'gc',以及函数'function'。下面是每个类型的具体描述:
Node.js性能重要事件节点(Performance Milestones)
加入性能时间轴的第一类PerformanceEntry条目叫作性能重要事件节点(Performance Milestones),类型是节点。这种特殊类型的条目记录了Node.js进程启动过程中重要事件发生的时间点。要读取这个特殊类型的条目有几种方法,但最快的是用perf_hooks.performance.nodeTime属性。
> perf_hooks.performance.nodeTiming
PerformanceNodeTiming {
duration: 4512.380027,
startTime: 158745518.63114,
entryType: 'node',
name: 'node',
arguments: 158745518.756349,
initialize: 158745519.09161,
inspectorStart: 158745522.408488,
loopStart: 158745613.442409,
loopExit: 0,
loopFrame: 158749857.025862,
bootstrapComplete: 158745613.439273,
third_party_main_start: 0,
third_party_main_end: 0,
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 }
目前这种类型条目支持的属性包括这些:
- 时长
duration:处于活动状态下的进程持续时间,单位是毫秒。 - 参数
arguments:命令行参数处理结束的时间点。 - 初始化
initialize:Node.js平台完成初始化的时间点。 - 检查工具开始
inspectorStart:Node.js检查工具启动完成的时间点。 - 循环开始
loopStart:Node.js事件循环开始的时间点。 - 循环退出
loopExit:Node.js事件循环退出的时间点。 - 循环帧
loopFrame:Node.js事件循环中当前一轮循环开始的时间点。 - 引导程序完成
bootstrapComplete:Node.js引导程序完成的时间点。 - 第三方主启动
third_party_main_start:第三方主模块处理过程启动的时间点。 - 第三方主完结
third_party_main_end: 第三方主模块处理过程完成的时间点。 - 进程簇设置开始
cluster_setup_start: 进程簇中子进程设置开始的时间。 - 进程簇设置结束
cluster_setup_end:进程簇中子进程设置结束的时间点。 - 模块载入开始
module_load_start: 本模块载入开始的时间点。 - 模块载入结束
module_load_end: 本模块载入结束的时间点。 - 预载入模块的载入开始
preload_modules_load_start:预载入模块载入开始的时间点。 - 预载入模块的载入结束
preload_modules_load_end:预载入模块载入结束的时间点。
Node.js事件循环(Event Loop)中的计时结果(Timing)
时间轴中加入的第二种PerformanceEntry条目类型是帧,它用于追踪记录Node.js事件循环的计时结果,可以用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 }
>
这里支持的属性包括:
- 每秒循环数
countsPerSecond:每秒事件循环数。 - 循环数
count:事件循环总数。 - 开始时间
startTime:当前一轮事件循环开始的时间点。 - 时长
duration:当前一轮事件循环的时长。 - 先前
prior:当前循环的前一轮事件循环所花时间,单位毫秒。
标记(Marks)与测量结果(Measures)
用户计时(User Timing)接口是性能时间轴的 扩展接口。这次实现的功能也支持这个接口,让用户能在时间轴上设定自己命名的标记,并测量标记之间的毫秒数。
比如:
const {
performance,
PerformanceObserver
} = require('perf_hooks');
performance.mark('A');
setImmediate(() => {
performance.mark('B');
performance.measure('A to C', 'A', 'B');
const measure = performance.getEntriesByName('A to C')[0];
console.log(measure.duration);
});
测量函数执行时间
这次实现的部分还支持对JavaScript函数执行时间的测量机制。
const {
performance,
PerformanceObserver
} = require('perf_hooks');
function myFunction() {}
const fn = performance.timerify(myFunction);
const obs = new PerformanceObserver((list) => {
console.log(list.getEntries()[0]);
obs.disconnect();
performance.clearFunctions();
});
obs.observe({ entryTypes: ['function'] });
fn(); // Call the timerified function
只要将PerformanceObserver观察类对象注册到类型为函数'function'的条目对象上,每次包裹其中的函数受到调用时就会计时,而计时所得的数据会加到性能时间轴上去。
特别有意思的一点,是这种功能也能用来对某个Node.js应用程序中所有的依存部分载入进行计时。
'use strict';
const {
performance,
PerformanceObserver
} = require('perf_hooks');
const mod = require('module');
// Monkey patch the require function
mod.Module.prototype.require =
performance.timerify(mod.Module.prototype.require);
require = performance.timerify(require);
// Activate the observer
const obs = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log(`require('${entry[0]}')`, entry.duration);
});
obs.disconnect();
// Free memory
performance.clearFunctions();
});
obs.observe({ entryTypes: ['function'], buffered: true });
require('some-module');
测量垃圾回收(Garbage Collection)时间
这次实现的部分还能使用户用PerformanceObserver观察类对象追踪记录垃圾回收事件。
const {
performance,
PerformanceObserver
} = require('perf_hooks');
const obs = new PerformanceObserver((list) => {
console.log(list.getEntries());
performance.clearGC();
});
obs.observe({ entryTypes: ['gc'] });
每次发生重大或非重大的垃圾回收活动时,只要PerformanceObserver注册在类型为'gc'的事件上,新的条目就会加到性能时间轴上去。
查询时间轴
要查看有哪些PerformanceEntry条目被加到了性能时间轴上,可以用GetEntries()函数,GetEntriesByName()函数,还有GetEntriesByType()函数。这些函数返回的值是一列PerformanceEntry对象实例,也可以选择对结果列进行筛选再返回。但是这些并不是监控时间轴的最有效率的手段。
每当一个PerformanceEntry条目加入时,PerformanceObserver类有一种接收后使用回调函数进行通知的方法。这种通知可以是同步发生的:
const {
performance,
PerformanceObserver
} = require('perf_hooks');
function callback(list, observer) {
console.log(list.getEntries());
observer.disconnect();
}
const obs = new PerformanceObserver(callback);
obs.observe(( entryTypes: ['mark', 'measure'] });
也可以加上buffered选项,非同步通知:
const {
performance,
PerformanceObserver
} = require('perf_hooks');
function callback(list, observer) {
console.log(list.getEntries());
observer.disconnect();
}
const obs = new PerformanceObserver(callback);
obs.observe(( entryTypes: ['mark', 'measure'], buffered: true });
上述例子中这个PerformanceObserver对象构造时收到了一个回调函数(callback)。无论什么时候,代码一旦调用了perf_hooks.performance.mark()标记函数或perf_hooks.performance.measure()测量函数,只要这个PerformanceObserver和它注册过了,那个回调函数就会受到触发。
只是一个很短的例子
新API接口有许多使用方法。下面这个例子里混用了Async_hooks接口和性能计时的接口来测量一个超时Timeout事件处理所花费的精确时间,包括从初始(Init)到消除(Destroy):
'use strict';
const async_hooks = require('async_hooks');
const {
performance,
PerformanceObserver
} = require('perf_hooks');
const set = new Set();
const hook = async_hooks.createHook({
init(id, type) {
if (type === 'Timeout') {
performance.mark(`Timeout-${id}-Init`);
set.add(id);
}
},
destroy(id) {
if (set.has(id)) {
set.delete(id);
performance.mark(`Timeout-${id}-Destroy`);
performance.measure(`Timeout-${id}`,
`Timeout-${id}-Init`,
`Timeout-${id}-Destroy`);
}
}
});
hook.enable();
const obs = new PerformanceObserver((list, observer) => {
console.log(list.getEntries()[0]);
performance.clearMarks();
performance.clearMeasures();
observer.disconnect();
});
obs.observe({ entryTypes: ['measure'], buffered: true });
setTimeout(() => console.log('test'), 1000);
细节可能有变动
我已经发出这个部分的合并请求。由于是新功能,所以某些细节可能会有改动。根据进度,我会尽量更新这个帖子的内容。
不想错过Node.js 合集的内容,在Medium注册吧。 了解更多