作者:theanarkh

打开网易新闻 查看精彩图片

前言:perf_hooks 是 Node.js 中用于收集性能数据的模块,Node.js 本身基于 perf_hooks 提供了性能数据,同时也提供了机制给用户上报性能数据。文本介绍一下 perk_hooks。

一、 使用

一、 使用

首先看一下 perf_hooks 的基本使用。

  • const { PerformanceObserver } = require('perf_hooks');
    const obs = new PerformanceObserver((items) => {

    obs.observe({ type: 'http' });1.2.3.4.5.

通过 PerformanceObserver 可以创建一个观察者,然后调用 observe 可以订阅对哪种类型的性能数据感兴趣。

下面看一下 C++ 层的实现,C++ 层的实现首先是为了支持 C++层的代码进行数据的上报,同时也为了支持 JS 层的功能。

二、 C++ 层实现

二、 C++ 层实现

1、 PerformanceEntry

1、 PerformanceEntry

PerformanceEntry 是 perf_hooks 里的一个核心数据结构,PerformanceEntry 代表一次性能数据。下面来看一下它的定义。

打开网易新闻 查看精彩图片

PerformanceEntry 里面记录了一次性能数据的信息,从定义中可以看到,里面记录了类型,开始时间,持续时间,比如一个 HTTP 请求的开始时间,处理耗时。除了这些信息之外,性能数据还包括一些额外的信息,由 details 字段保存,比如 HTTP 请求的 url,不过目前还不支持这个能力,不同的性能数据会包括不同的额外信息,所以 PerformanceEntry 是一个类模版,具体的 details 由具体的性能数据生产者实现。下面我们看一个具体的例子。

打开网易新闻 查看精彩图片

这是关于 gc 性能数据的实现,我们看到它的 details 里包括了 kind 和 flags。接下来看一下 perf_hooks 是如何收集 gc 的性能数据的。首先通过 InstallGarbageCollectionTracking 注册 gc 钩子。

打开网易新闻 查看精彩图片

InstallGarbageCollectionTracking 主要是使用了 V8 提供的两个函数注册了 gc 开始和 gc 结束阶段的钩子。我们看一下这两个钩子的逻辑。

打开网易新闻 查看精彩图片

MarkGarbageCollectionStart 在开始 gc 时被执行,逻辑很简单,主要是记录了 gc 的开始时间。接着看 MarkGarbageCollectionEnd。

打开网易新闻 查看精彩图片

MarkGarbageCollectionEnd 根据刚才记录 gc开始时间,计算出 gc 的持续时间。然后产生一个性能数据 GCPerformanceEntry。然后在事件循环的 check 阶段通过 Notify 进行上报。

打开网易新闻 查看精彩图片

Notify 进行进一步的处理,然后执行JS 的回调进行数据的上报。env->performance_entry_callback() 对应的回调在 JS 设置。

2、 PerformanceState

2、 PerformanceState

PerformanceState 是 perf_hooks 的另一个核心数据结构,负责管理 perf_hooks 模块的一些公共数据。

打开网易新闻 查看精彩图片

PerformanceState 主要是记录了 Node.js 初始化时的性能数据,比如 Node.js 初始化完毕的时间,事件循环的开始时间等。还有就是记录了观察者的数据结构,比如对 HTTP 性能数据感兴趣的观察者,主要用于控制要不要上报相关类型的性能数据。比如如果没有观察者的话,那么就不需要上报这个数据。

三、 JS 层实现

三、 JS 层实现

接下来看一下 JS 的实现。首先看一下观察者的实现。

  • class PerformanceObserver {
    constructor(callback) {
    // 性能数据
    this[kBuffer] = [];
    // 观察者订阅的性能数据类型
    this[kEntryTypes] = new SafeSet();
    // 观察者对一个还是多个性能数据类型感兴趣
    this[kType] = undefined;
    // 观察者回调
    this[kCallback] = callback;
    observe(options = {}) {
    const {
    entryTypes,
    type,
    buffered,
    } = { ...options };
    // 清除之前的数据
    maybeDecrementObserverCounts(this[kEntryTypes]);
    this[kEntryTypes].clear();
    // 重新订阅当前设置的类型
    for (let n = 0; n < entryTypes.length; n++) {
    if (ArrayPrototypeIncludes(kSupportedEntryTypes, entryTypes[n])) {
    this[kEntryTypes].add(entryTypes[n]);
    maybeIncrementObserverCount(entryTypes[n]);

    // 插入观察者队列
    kObservers.add(this);
    takeRecords() {
    const list = this[kBuffer];
    this[kBuffer] = [];
    return list;
    static get supportedEntryTypes() {
    return kSupportedEntryTypes;
    // 产生性能数据时被执行的函数
    [kMaybeBuffer](entry) {
    if (!this[kEntryTypes].has(entry.entryType))
    return;
    // 保存性能数据,迟点上报
    ArrayPrototypePush(this[kBuffer], entry);
    // 插入待上报队列
    kPending.add(this);
    if (kPending.size)
    queuePending();
    // 执行观察者回调
    [kDispatch]() {
    this[kCallback](new PerformanceObserverEntryList(this.takeRecords()),
    this);
    }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.
  • 观察者的实现比较简单,首先有一个全局的变量记录了所有的观察者,然后每个观察者记录了自己订阅的类型。当产生性能数据时,生产者就会通知观察者,接着观察者执行回调。这里需要额外介绍的一个是 maybeDecrementObserverCounts 和 maybeIncrementObserverCount。
  • function getObserverType(type) {
    switch (type) {
    case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC;
    case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2;
    case 'http': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP;

    function maybeDecrementObserverCounts(entryTypes) {
    for (const type of entryTypes) {
    const observerType = getObserverType(type);
    if (observerType !== undefined) {
    observerCounts[observerType]--;
    if (observerType === NODE_PERFORMANCE_ENTRY_TYPE_GC &&
    observerCounts[observerType] === 0) {
    removeGarbageCollectionTracking();
    gcTrackingInstalled = false;

    }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

maybeDecrementObserverCounts 主要用于操作 C++ 层的逻辑,首先根据订阅类型判断是不是 C++ 层支持的类型,因为 perf_hooks 在 C++ 和 JS 层都定义了不同的性能类型,如果是涉及到底层的类型,就会操作 observerCounts 记录当前类型的观察者数量,observerCounts 就是刚才分析 C++ 层的 observers 变量,它是一个数组,每个索引对应一个类型,数组元素的值是观察者的个数。另外如果订阅的是 gc 类型,并且是第一个订阅者,那就 JS 层就会操作 C++ 层往 V8 里注册 gc 回调。

了解了 perf_hooks 提供的机制后,我们来看一个具体的性能数据上报例子。这里以 HTTP Server 处理请求的耗时为例。

  • function emitStatistics(statistics) {
    const startTime = statistics.startTime;
    const diff = process.hrtime(startTime);
    const entry = new InternalPerformanceEntry(
    statistics.type,
    'http',
    startTime[0] * 1000 + startTime[1] / 1e6,
    diff[0] * 1000 + diff[1] / 1e6,
    undefined,
    enqueue(entry);
    }1.2.3.4.5.6.7.8.9.10.11.12.

下面是 HTTP Server 处理完一个请求时上报性能数据的逻辑。首先创建一个 InternalPerformanceEntry 对象,这个和刚才介绍的 C++ 对象是一样的,是表示一个性能数据的对象。接着调用 enqueue 函数。

  • function enqueue(entry) {
    // 通知观察者有性能数据,观察者自己判断是否订阅了这个类型的数据
    for (const obs of kObservers) {
    obs[kMaybeBuffer](entry);
    // 如果是 mark 或 measure 类型,则插入一个全局队列。
    const entryType = entry.entryType;
    let buffer;
    if (entryType === 'mark') {
    buffer = markEntryBuffer; // mark 性能数据队列
    } else if (entryType === 'measure') {
    buffer = measureEntryBuffer; // measure 性能数据队列
    } else {
    return;
    ArrayPrototypePush(buffer, entry);
    }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.

enqueue 会把性能数据上报到观察者,然后观察者如果订阅这个类型的数据则执行用户回调通知用户。我们看一下 obs[kMaybeBuffer] 的逻辑。

  • [kMaybeBuffer](entry) {
    if (!this[kEntryTypes].has(entry.entryType))
    return;
    ArrayPrototypePush(this[kBuffer], entry);
    // this 是观察者实例
    kPending.add(this);
    if (kPending.size)
    queuePending();
    function queuePending() {
    if (isPending) return;
    isPending = true;
    setImmediate(() => {
    isPending = false;
    const pendings = ArrayFrom(kPending.values());
    kPending.clear();
    // 遍历观察者队列,执行 kDispatch
    for (const pending of pendings)
    pending[kDispatch]();

    // 下面是观察者中的逻辑,观察者把当前保存的数据上报给用户
    [kDispatch]() {
    this[kCallback](new PerformanceObserverEntryList(this.takeRecords()),this);
    }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.

另外 mark 和 measure 类型的性能数据比较特殊,它不仅会通知观察者,还会插入到全局的一个队列中。所以对于其他类型的性能数据,如果没有观察者的话就会被丢弃(通常在调用 enqueue 之前会先判断是否有观察者),对于 mark 和 measure 类型的性能数据,不管有没有观察者都会被保存下来,所以我们需要显式清除。

四、 总结

四、 总结

以上就是 perf_hooks 中核心的实现,除此之外,perf_hooks 还提供了其他的功能,本文就先不介绍了。可以看到 perf_hooks 的实现是一个订阅发布的模式,看起来貌似没什么特别的。但是它的强大之处在于是由 Node.js 内置实现的, 这样 Node.js 的其他模块就可以基于 perf_hooks 这个框架上报各种类型的性能数据。相比来说虽然我们也能在用户层实现这样的逻辑,但是我们拿不到或者没有办法优雅地方法拿到 Node.js 内核里面的数据,比如我们想拿到 gc 的性能数据,我们只能写 addon 实现。又比如我们想拿到 HTTP Server 处理请求的耗时,虽然可以通过监听 reqeust 或者 response 对象的事件实现,但是这样一来我们就会耦合到业务代码里,每个开发者都需要处理这样的逻辑,如果我们想收拢这个逻辑,就只能劫持 HTTP 模块来实现,这些不是优雅但是是不得已的解决方案。有了 perf_hooks 机制,我们就可以以一种结耦的方式来收集这些性能数据,实现写一个 SDK,大家只需要简单引入就行。

最近在研究 perf_hooks 代码的时候发现目前 perf_hooks 的功能还不算完善,很多性能数据并没有上报,目前只支持 HTTP Server 的请求耗时、HTTP 2 和 gc 耗时这些性能数据。所以最近提交了两个 PR 支持了更多性能数据的上报。第一个 PR 是用于支持收集 HTTP Client 的耗时,第二个 PR 是用于支持收集 TCP 连接和 DNS 解析的耗时。在第二个 PR 里,实现了两个通用的方法,方便后续其他模块做性能上报。另外后续有时间的话,希望可以去不断完善 perf_hooks 机制和性能数据收集这块的能力。在从事 Node.js 调试和诊断这个方向的这段时间里,深感到应用层能力的局限,因为我们不是业务方,而是基础能力的提供者,就像前面提到的,哪怕想提供一个收集 HTTP 请求耗时的数据都是非常困难的,而作为基础能力的提供者,我们一直希望我们的能力对业务来说是无感知,无侵入并且是稳定可靠的。所以我们需要不断深入地了解 Node.js 在这方面提供的能力,如果 Node.js 没有提供我们想要的功能,我们只能写 addon 或者尝试给社区提交 PR 来解决。另外我们也在慢慢了解和学习 ebpf,希望能利用 ebpf 从另外一个层面帮助我们解决所碰到的问题。

最后附上两个 PR 的地址,有兴趣的同学可以了解下。在内核层面这块还是有很多事情可以做,希望未来 Node.js 在这块能力越来也强大。

https://github.com/nodejs/node/pull/42345。

https://github.com/nodejs/node/pull/42390。

责任编辑:姜华来源: 编程杂技