前言: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 Traits>struct PerformanceEntry {
using Details = typename Traits::Details;
std::string name;
double start_time;
double duration;
Details details;
static v8::MaybeLocal::Object> GetDetails(
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::MaybeLocal::Object> GetDetails(
Environment* env,
const PerformanceEntry>& entry);
};
using GCPerformanceEntry = PerformanceEntry>;
这是关于 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_ptr> entry =
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::Local::Object> detail;
if (!Traits::GetDetails(env, *this).ToLocal(&detail)) {
// TODO(): Handle the error here
return;
}
v8::Local::Value> argv[] = {
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。