您现在的位置是:亿华云 > IT科技类资讯

一篇带给你Node.js 的Perf_Hooks

亿华云2025-10-03 17:44:26【IT科技类资讯】5人已围观

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

前言: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 });

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

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

二、篇带 C++ 层实现

1、篇带 PerformanceEntry

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

template struct PerformanceEntry {

using Details = typename Traits::Details;

std::string name;

double start_time;

double duration;

Details details;

static v8::MaybeLocalGetDetails(

Environment* env,

const PerformanceEntry& entry) {

return Traits::GetDetails(env, entry);

}

};

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

struct GCPerformanceEntryTraits {

static constexpr PerformanceEntryType kType = NODE_PERFORMANCE_ENTRY_TYPE_GC;

struct Details {

PerformanceGCKind kind;

PerformanceGCFlags flags;

Details(PerformanceGCKind kind_, PerformanceGCFlags flags_)

: kind(kind_), flags(flags_) { }

};

static v8::MaybeLocalGetDetails(

Environment* env,

const PerformanceEntry& entry);

};

using GCPerformanceEntry = PerformanceEntry<GCPerformanceEntryTraits>;

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

static void InstallGarbageCollectionTracking(const FunctionCallbackInfo& args) {

Environment* env = Environment::GetCurrent(args);

env->isolate()->AddGCPrologueCallback(MarkGarbageCollectionStart, static_cast(env));

env->isolate()->AddGCEpilogueCallback(MarkGarbageCollectionEnd, static_cast(env));

env->AddCleanupHook(GarbageCollectionCleanupHook, env);

}

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

void MarkGarbageCollectionStart(

Isolate* isolate,

GCType type,

GCCallbackFlags flags,

void* data) {

Environment* env = static_cast(data);

env->performance_state()->performance_last_gc_start_mark = PERFORMANCE_NOW();

}

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

void MarkGarbageCollectionEnd(

Isolate* isolate,

GCType type,

GCCallbackFlags flags,

void* data) {

Environment* env = static_cast(data);

PerformanceState* state = env->performance_state();

double start_time = state->performance_last_gc_start_mark / 1e6;

double duration = (PERFORMANCE_NOW() / 1e6) - start_time;

std::unique_ptrentry =

std::make_unique(

"gc",

start_time,

duration,

GCPerformanceEntry::Details(

static_cast(type),

static_cast(flags)));

env->SetImmediate([entry = std::move(entry)](Environment* env) {

entry->Notify(env);

}, CallbackFlags::kUnrefed);

}

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

void Notify(Environment* env) {

v8::Localdetail;

if (!Traits::GetDetails(env, *this).ToLocal(&detail)) {

// TODO(@jasnell): Handle the error here

return;

}

v8::Localargv[] = {

OneByteString(env->isolate(), name.c_str()),

OneByteString(env->isolate(), GetPerformanceEntryTypeName(Traits::kType)),

v8::Number::New(env->isolate(), start_time),

v8::Number::New(env->isolate(), duration),

detail

};

node::MakeSyncCallback(

env->isolate(),

env->context()->Global(),

env->performance_entry_callback(),

arraysize(argv),

argv);

}

};

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

2、 PerformanceState

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

class PerformanceState {

public:

explicit PerformanceState(v8::Isolate* isolate, const SerializeInfo* info);

AliasedUint8Array root;

AliasedFloat64Array milestones;

AliasedUint32Array observers;

uint64_t performance_last_gc_start_mark = 0;

void Mark(enum PerformanceMilestone milestone,uint64_t ts = PERFORMANCE_NOW());

private:

struct performance_state_internal {

// Node.js 初始化时的性能数据

double milestones[NODE_PERFORMANCE_MILESTONE_INVALID];

// 记录对不同类型性能数据感兴趣的观察者个数

uint32_t observers[NODE_PERFORMANCE_ENTRY_TYPE_INVALID];

};

};

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

三、 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);

}

}

观察者的实现比较简单,首先有一个全局的变量记录了所有的观察者,然后每个观察者记录了自己订阅的类型。当产生性能数据时,生产者就会通知观察者,接着观察者执行回调。这里需要额外介绍的一个是 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;

}

}

}

}

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);

}

下面是 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);

}

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);

}

另外 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。

很赞哦!(79634)