我们知道 Node.js 是基于 V8 引擎的,V8 暴露了一些 profiler API,我们可以通过 v8-profiler 收集一些运行时数据(例如:CPU 和内存)。本节将介绍如何使用 v8-profiler 分析 CPU 的使用情况。
创建测试代码:
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。这里我们不深究每个字段的含义,先来看看如何可视化这些数据。
Chrome 自带了分析 CPU profile 日志的工具。打开 Chrome -> 调出开发者工具(DevTools) -> 单击右上角三个点的按钮 -> More tools -> JavaScript Profiler -> Load,加载刚才生成的 cpuprofile 文件。左上角的下拉菜单可以选择如下三种模式:
- Chart:显示按时间顺序排列的火焰图。
- Heavy (Bottom Up):按照函数对性能的影响排列,同时可以检查函数的调用路径。
- Tree (Top Down):显示调用结构的总体状况,从调用堆栈的顶端开始。
这里我们选择 Tree (Top Down) 模式,按 Total Time 降序排列。可以看到有如下三列:
- Self Time:函数调用所耗费的时间,仅包含函数本身的声明,不包含任何子函数的执行时间。
- Total Time:函数调用所耗费的总时间,包含函数本身的声明及所有子函数执行时间。即:父函数的 Total Time = 父函数的 Self Time + 所有子函数的 Total Time。
- Function:函数名及路径,可展开查看子函数。
我们不断地展开,并定位到了 encryptRouter,如下图所示:
可以看出:我们定位到了 encryptRouter 这个路由,并且这个路由中 exports.pbkdf2Sync 占据了绝大部分 CPU 时间。
我们也可以用火焰图来展示 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 时间。
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。