在实际 Node.js
应用开发中可能会遇到性能上的瓶颈,在项目比较复杂的时候光阅读代码是很难发现问题所在的,这时候就需要有效的方法来发现瓶颈所在。
这里介绍一种简单的方法来帮助开发者洞察瓶颈所在,提升 Node.js
应用性能。
这个方法的主要目标就是度量在 Node.js
应用中执行每个函数所花费的 CPU
时间。当然也可以通过对内存的度量来检测内存泄漏,但在本篇文章只介绍对性能上面的优化。
度量
谷歌浏览器可以通过 DevTools
来记录每个函数信息和执行时间并将这些东西打进日志文件,来帮助开发者来发现瓶颈。而 Node.js
也有类似的内置工具,就是 --prof
配置,这个可以统计所有函数执行消耗的 CPU
时间片长度。
先来看个简单的例子:
1 | // file test.js |
上面的代码的主要功能是将一区间的整数分成几个部分分别对每 4 个数做加减乘除运算,最后将每个部分的结果加合得到最后的结果。可以发现上面代码为了提高性能而使用多个 CPU
核去执行计算任务,想充分利用 CPU
的性能,然而事实并非如此,在我的机器上运行结果如下:
1 | time: 1369 |
再来看看只用一个 CPU
来执行的代码和结果:
1 | // file test2.js |
结果:
1 | time: 541 |
可以发现使用了多个 CPU
来执行反而更慢了,这是为啥?我们用上面说的配置来看一下问题出在哪。运行:
1 | node --prof test.js |
执行完后出现一堆类似于 isolate-*-v8-*.log
的子进程 log(共 8 个,子进程 worker
) 和一个主进程 log isolate-*-v8.log
(主进程 master
)。
分析
使用 --prof-process
配置运行 NodeJS
,并提供上面生成的文件的路径。
1 | node --prof-process isolate-0x102802400-v8-81626.log |
从上到下主要分为几个耗时分类,Shared libraries
, JavaScript
, C++
, Summary
, C++ entry points
, Bottom up (heavy) profile
:
Shared libraries
Node
进程使用到的系统级动态链接库部分的时间消耗,会显示在这个分类下。
该分类的几列:
ticks
:每个库所占用的 ticks
数量total
:每个库占用的 ticks
总量百分比nonlib
:这列在当前分类不适用,因为本来这里列的就都是类库时间消耗,nonlib
当然没有数据name
:动态链接库的文件位置
JavaScript、C++、Summary
JavaScript
JavaScript
代码部分的时间消耗,包括了当前项目源代码部分的时间消耗和第三方 node_modules
的时间消耗。
C++
Node
进程在 C++
代码里的时间消耗,Node
本身是构建在 V8
引擎之上的,所以一些 Node
标准库里的 API
,基本上都是 C++
时间消耗。当然这个分类也包含了一些作为第三方 addon
加载的插件的时间消耗。
Summary
将所有的分类的时间消耗总量都放在一起,形成一个直观的结果。
列含义
ticks
:占用的ticks
数量total
:占用的ticks
总量百分比nonlib
:
这列描述的是将Shared libraries
所产生的时间消耗忽略之后,当前条目自身产生的时间消耗(ticks
)所占的百分比name
(每个name
列实际函数名之前一般会有一个*
或~
,星号表示该函数得到了优化,而波浪号则表示没有):JavaScript
:函数名,以及其在源代码中的位置C++
:函数名,一般都是Node
运行时和V8
相关的函数Summary
:分类名
C++ entry points
这部分描述的是当逻辑从 JS
代码跨界到 C++
代码运行时,其中消耗的时间。
Bottom up (heavy) profile
这部分是性能问题的暴露部分,一般看完 Summery
不想了解其细节就直接来看这部分是解决问题的最快方案。
和之前其他分类耗时部分不同的是,在这部分里按空行分隔的不同段落都是一个个单独的性能瓶颈点,每个段落的多行表示的是一个调用栈。
此外,这个部分的列内容也和之前的略有不同(主要是parent
字段),parent
列的百分比意味着:表示上一行中的函数由当前行中的函数调用的百分比。
具体分析
先看 Summary
:
1 | [Summary]: |
可以发现大部分性能浪费在执行 C++
代码上了,所以我们重点观察 C++
部分。
1 | [C++]: |
34%的耗时都在 StringToArrayIndex
,而调用该函数的都是 Runtime_StringToNumber
,根据调用栈我们就发现代码中有一段 for (let i = Number(start); i < Number(end); i++)
发现问题所在,是循环的时候多次处理了 end
,所以只需要将 Number(end)
放在循环外处理就行了:
1 | start = Number(start) |
改完再分析结果如下:
1 | [Summary]: |
可以发现耗时的 StringToNumber
已经没有了,CPU
的 tick
也只有119了。