我们知道 Node.js 是基于 V8 引擎的,V8 暴露了一些 profiler API,我们可以通过 v8-profiler 收集一些运行时数据(例如:CPU 和内存)。本节将介绍如何使用 v8-profiler 分析 CPU 的使用情况。

1.2.1 使用 v8-profiler

创建测试代码:

app.js

  1. const fs = require('fs')
  2. const crypto = require('crypto')
  3. const Bluebird = require('bluebird')
  4. const profiler = require('v8-profiler')
  5. const Paloma = require('paloma')
  6. const app = new Paloma()
  7. app.route({ method: 'GET', path: '/encrypt', controller: function encryptRouter (ctx) {
  8. const password = ctx.query.password || 'test'
  9. const salt = crypto.randomBytes(128).toString('base64')
  10. const encryptedPassword = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex')
  11. ctx.body = encryptedPassword
  12. }})
  13. app.route({ method: 'GET', path: '/cpuprofile', async controller (ctx) {
  14. //Start Profiling
  15. profiler.startProfiling('CPU profile')
  16. await Bluebird.delay(30000)
  17. //Stop Profiling after 30s
  18. const profile = profiler.stopProfiling()
  19. profile.export()
  20. .pipe(fs.createWriteStream(`cpuprofile-${Date.now()}.cpuprofile`))
  21. .on('finish', () => profile.delete())
  22. ctx.status = 204
  23. }})
  24. app.listen(3000)

GET /encrypt 有一个 CPU 密集型的计算函数 crypto.pbkdf2Sync,GET /cpuprofile 用来收集 30s 的 V8 log 然后将其 dump 到一个文件中。

运行该程序,打开两个终端窗口。一个终端运行:

  1. $ curl localhost:3000/cpuprofile

来触发 CPU profiling,然后另一个终端立即运行:

  1. $ ab -c 20 -n 2000 "http://localhost:3000/encrypt?password=123456"

来触发 CPU 密集计算。

最后生成 cpuprofile-xxx.cpuprofile 文件,该文件的内容其实就是一个大的 JSON 对象,大体如下:

  1. {
  2. "typeId": "CPU",
  3. "uid": "1",
  4. "title": "CPU profile",
  5. "head":
  6. { "functionName": "(root)",
  7. "url": "",
  8. "lineNumber": 0,
  9. "callUID": 154,
  10. "bailoutReason": "",
  11. "id": 1,
  12. "scriptId": 0,
  13. "hitCount": 0,
  14. "children": [ ... ] },
  15. "startTime": 276245,
  16. "endTime": 276306,
  17. "samples": [ ... ],
  18. "timestamps": [ ... ]
  19. }

这个 JSON 对象记录了函数调用栈、路径、时间戳和其他一些信息,samples 节点数组与 timestamps 节点数组中的时间戳是一一对应的,并且 samples 节点数组中的每一个值,其实对应了 head 节点的深度优先遍历 ID。这里我们不深究每个字段的含义,先来看看如何可视化这些数据。

1.2.2 方法 1——Chrome DevTools

Chrome 自带了分析 CPU profile 日志的工具。打开 Chrome -> 调出开发者工具(DevTools) -> 单击右上角三个点的按钮 -> More tools -> JavaScript Profiler -> Load,加载刚才生成的 cpuprofile 文件。左上角的下拉菜单可以选择如下三种模式:

  1. Chart:显示按时间顺序排列的火焰图。
  2. Heavy (Bottom Up):按照函数对性能的影响排列,同时可以检查函数的调用路径。
  3. Tree (Top Down):显示调用结构的总体状况,从调用堆栈的顶端开始。

这里我们选择 Tree (Top Down) 模式,按 Total Time 降序排列。可以看到有如下三列:

  1. Self Time:函数调用所花的时间,仅包含函数本身的声明,不包含任何子函数的执行时间。
  2. Total Time:函数调用所花的总时间,包含函数本身的声明及所有子函数执行时间。即:父函数的 Total Time = 父函数的 Self Time + 所有子函数的 Total Time。
  3. Function:函数名及路径,可展开查看子函数。

我们不断地展开,并定位到了 encryptRouter,如下图所示:

v8-profiler - 图1

可以看出:我们定位到了 encryptRouter 这个路由,并且这个路由中 exports.pbkdf2Sync 占据了绝大部分 CPU 时间。

1.2.3 方法 2——火焰图

我们也可以用火焰图来展示 cpuprofile 数据。首先全局安装 flamegraph 这个模块:

  1. $ npm i flamegraph -g

运行以下命令将 cpuprofile 文件生成 svg 文件:

  1. $ flamegraph -t cpuprofile -f cpuprofile-xxx.cpuprofile -o cpuprofile.svg

浏览器打开 cpuprofile.svg,如下所示:

v8-profiler - 图2

可以看出:我们定位到了 app.js 的第 8 行,即 encryptRouter 这个路由,并且这个路由中 exports.pbkdf2Sync 占据了绝大部分 CPU 时间。

1.2.4 方法 3——v8-analytics

v8-analytics 是社区开源的一个解析 v8-profiler 和 heapdump 等模块生成的 CPU 和 heap-memory 日志的工具。它提供以下功能:

  • 将 V8 引擎逆优化或者优化失败的函数标红展示,并展示优化失败的原因。
  • 在函数执行时长超过预期时标红展示。
  • 展示当前项目中可疑的内存泄漏点。

我们以上述第 2 条功能为例,使用 v8-analytics 分析 CPU 的使用情况。

首先,全局安装 v8-analytics:

  1. $ npm i v8-analytics -g

使用以下命令只查看执行时间大于 200ms 的函数:

  1. $ va timeout cpuprofile-xxx.cpuprofile 200 --only

结果截图如下:

v8-profiler - 图3

可以看出:我们依然能够定位到 encryptRouter 和 exports.pbkdf2Sync。

1.2.5 参考链接

上一节:1.1 perf + FlameGraph

下一节:1.3 Tick Processor