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

1.2.1 使用 v8-profiler

创建测试代码:

app.js

const fs = require('fs')
const crypto = require('crypto')
const Bluebird = require('bluebird')
const profiler = require('v8-profiler')
const Paloma = require('paloma')
const app = new Paloma()

app.route({ method: 'GET', path: '/encrypt', controller: function encryptRouter (ctx) {
  const password = ctx.query.password || 'test'
  const salt = crypto.randomBytes(128).toString('base64')
  const encryptedPassword = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex')

  ctx.body = encryptedPassword
}})

app.route({ method: 'GET', path: '/cpuprofile', async controller (ctx) {
   //Start Profiling
   profiler.startProfiling('CPU profile')
   await Bluebird.delay(30000)
   //Stop Profiling after 30s
   const profile = profiler.stopProfiling()
   profile.export()
     .pipe(fs.createWriteStream(`cpuprofile-${Date.now()}.cpuprofile`))
     .on('finish', () => profile.delete())
   ctx.status = 204
}})

app.listen(3000)

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

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

$ curl localhost:3000/cpuprofile

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

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

来触发 CPU 密集计算。

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

{
  "typeId": "CPU",
  "uid": "1",
  "title": "CPU profile",
  "head":
   { "functionName": "(root)",
     "url": "",
     "lineNumber": 0,
     "callUID": 154,
     "bailoutReason": "",
     "id": 1,
     "scriptId": 0,
     "hitCount": 0,
     "children": [ ... ] },
  "startTime": 276245,
  "endTime": 276306,
  "samples": [ ... ],
  "timestamps": [ ... ]
}

这个 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,如下图所示:

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

1.2.3 方法 2——火焰图

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

$ npm i flamegraph -g

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

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

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

可以看出:我们定位到了 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:

$ npm i v8-analytics -g

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

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

结果截图如下:

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

1.2.5 参考链接

上一节:1.1 perf + FlameGraph

下一节:1.3 Tick Processor

results matching ""

    No results matching ""