网易首页 > 网易号 > 正文 申请入驻

一篇带给你Node.js 的Perf_Hooks

0
分享至

作者: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++ 层实现
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

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

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

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

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
大暴雨!冰雹!8级雷暴大风!首个山洪红色预警!气象部门紧急提醒→

大暴雨!冰雹!8级雷暴大风!首个山洪红色预警!气象部门紧急提醒→

鲁中晨报
2024-06-16 14:43:05
伊朗态度大变?突然召见中国大使,当面发出抗议,中方回应亮了!

伊朗态度大变?突然召见中国大使,当面发出抗议,中方回应亮了!

诉人世间
2024-06-17 00:30:02
90年代台海危机中的心酸:中国空军苏-27战斗机,吓人的花瓶

90年代台海危机中的心酸:中国空军苏-27战斗机,吓人的花瓶

慎独赢
2024-06-16 11:51:15
理想车友聚会多车连环追尾的瓜

理想车友聚会多车连环追尾的瓜

一个岛岛
2024-06-16 16:42:12
镜报:曼城老板与瓜迪奥拉会面,希望说服他与俱乐部续约

镜报:曼城老板与瓜迪奥拉会面,希望说服他与俱乐部续约

直播吧
2024-06-16 09:57:12
不打了?CBA最强外援被曝欲离队,广东男篮有望重金签下他!

不打了?CBA最强外援被曝欲离队,广东男篮有望重金签下他!

绯雨儿
2024-06-16 16:16:04
大陆男子驾艇成功登台湾细节曝光:突破5亿监控系统后,自己报警

大陆男子驾艇成功登台湾细节曝光:突破5亿监控系统后,自己报警

消失的电波
2024-06-13 10:01:58
《玫瑰的故事》那么明显的穿帮镜头,竟没有剪掉,看着真是一脸懵

《玫瑰的故事》那么明显的穿帮镜头,竟没有剪掉,看着真是一脸懵

娱乐圈笔娱君
2024-06-14 17:55:48
国家终于不再原谅王濛,77枚金牌不是万能,而自大只会被抛弃

国家终于不再原谅王濛,77枚金牌不是万能,而自大只会被抛弃

兰子记
2024-06-11 18:28:00
国足豪赌世界杯!足协锁定巴西外援 最快1月完成归化

国足豪赌世界杯!足协锁定巴西外援 最快1月完成归化

球事百科吖
2024-06-16 16:34:09
谭咏麟病愈后首次公开现身,瘦到青筋毕现感慨声线不好

谭咏麟病愈后首次公开现身,瘦到青筋毕现感慨声线不好

小萝卜天下事
2023-07-21 21:57:53
把150万给儿子,女儿一家没了音讯,10年后我们在女儿旧房前痛哭

把150万给儿子,女儿一家没了音讯,10年后我们在女儿旧房前痛哭

半夏解语
2024-06-15 07:00:03
3:0波兰,龚翔宇大哭原因曝光,李盈莹背后轻抱,低迷之人该清醒

3:0波兰,龚翔宇大哭原因曝光,李盈莹背后轻抱,低迷之人该清醒

东球弟
2024-06-16 21:45:46
拜登已签字,22国一致对华征税,中方加量囤粮食,瑞典的努力白费

拜登已签字,22国一致对华征税,中方加量囤粮食,瑞典的努力白费

影视解说阿相
2024-06-15 12:39:36
中国女排3比0波兰数据:队长袁心玥16分最高,李盈莹14分次席

中国女排3比0波兰数据:队长袁心玥16分最高,李盈莹14分次席

直播吧
2024-06-16 22:10:57
G7为何敢用冻结俄资产做担保为乌提供500亿,因为俄T-62坦克上场

G7为何敢用冻结俄资产做担保为乌提供500亿,因为俄T-62坦克上场

山河路口
2024-06-15 23:54:24
一个上市公司能无耻到什么程度呢?

一个上市公司能无耻到什么程度呢?

流苏晚晴
2024-06-14 20:24:29
塞尔维亚1-1英格兰,弗拉霍维奇建功,凯恩哑火,欧洲中国队丢人

塞尔维亚1-1英格兰,弗拉霍维奇建功,凯恩哑火,欧洲中国队丢人

邮轮摄影师阿嗵
2024-06-16 20:11:29
外媒:以色列面对真主党左右为难

外媒:以色列面对真主党左右为难

参考消息
2024-06-16 09:57:07
“这里不是中国!不会有人惯你们!”中国大妈已经沦落成世界公害

“这里不是中国!不会有人惯你们!”中国大妈已经沦落成世界公害

三月柳
2024-06-01 15:24:12
2024-06-17 01:48:49
Nodejs开发
Nodejs开发
分享只有程序员懂的干货
648文章数 824关注度
往期回顾 全部

科技要闻

iPhone 16会杀死大模型APP吗?

头条要闻

南方医院回应教师因救人迟到:教学差错是最轻档处理

头条要闻

南方医院回应教师因救人迟到:教学差错是最轻档处理

体育要闻

没人永远年轻 但青春如此无敌还是离谱了些

娱乐要闻

上影节红毯:倪妮好松弛,娜扎吸睛

财经要闻

打断妻子多根肋骨 上市公司创始人被公诉

汽车要闻

售17.68万-21.68万元 极狐阿尔法S5正式上市

态度原创

教育
健康
游戏
数码
公开课

教育要闻

大家都知道,这题目不会特别容易,看来还得是稳打稳的刷题才行啊

晚餐不吃or吃七分饱,哪种更减肥?

梦幻西游玩家炸出160愤怒水清腰带,西栅为服战连夜换“网吧”?

数码要闻

PCIe 5.0 SSD终于要便宜了!群联E31T主控无缓存能跑12GB/s

公开课

近视只是视力差?小心并发症

无障碍浏览 进入关怀版