chaussen

摘自Medium网站:Node.js合集之计时就是一切

原文链接: medium.com

作者James M Snell的信息

作者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注册吧。 了解更多