Node.js 函数性能剖析






4.67/5 (2投票s)
无需修改代码,即可深入了解您的 Node.js 代码性能。快速了解哪些函数占用了最多的 CPU 时间。
引言
市面上有许多 Node.js 的性能剖析器。它们中的大多数允许您接入 v8 日志并提取 CPU 时间、内存使用情况等性能数据,它们大多关注代码运行的环境,而不会按文件/函数提供数据。其他库则要求您手动添加标记来计时。还有一些库提供“自动”钩子,但它们在复制被覆盖(包装)函数的函数签名方面存在一个主要缺陷。
node.profiler
(node dot profiler)有何不同?
- 它按(文件名/函数名)提供清晰有用的数据。
- 它不需要您修改代码或添加标记。
- 它动态地钩入您的代码。
- 它创建的钩子尽可能保留了原始函数的行为和签名。
- 它能处理
async
函数。(回调)。
背景
我工作的公司需要对代码进行一些严肃的性能剖析,因此我们调研了许多现有工具。V8 日志不是我们想要的,我们需要知道哪个函数被调用了,调用了多少次,以及在函数内部花费了多少时间。有一些库,如 nodetime
和 async-profiler
。nodetime
在不手动标记代码的情况下无法提供详细信息。async-profiler
仍然需要一些代码更改,并且它使用的包装器(shimmer)不能忠实地复制函数签名。在我们的代码中,我们使用了高级依赖注入模式,这些模式依赖于定义的函数参数,并且我们还通过从函数代码内部提取特殊注释来使用一些高级元数据。总之,我们需要包装器在覆盖函数时尽可能忠实。覆盖函数是性能剖析工具的关键。
node.profiler
解决了这些挑战。
使用 node.profiler
前往 https://npmjs.net.cn/package/node.profiler 获取该包,或者直接使用以下命令安装:
$ npm install -g node.profiler
在您的 nodejs 代码文件夹中,只需运行 node.profiler
/path/to/your/project/ $ node.profiler
node.profiler
将以监控模式运行您的项目。
按需使用您的项目,然后退出项目,node.profiler
将为您创建性能数据(当代码空闲时,它还会每隔 X 时间刷新一次数据)。
输出
node.profiler
的核心代码将输出 "node.profiler.json" 文件。该文件将包含一个 JSON 对象,每个属性都是一个文件名/函数名,属性内部将包含显示函数被调用总次数以及函数内部总耗时的数据。时间以毫秒为单位,精度为纳秒。
{
"/path/to/your/project/lib/index.js:functionSync": {
"async": {
"count": 0,
"total": 0
},
"sync": {
"count": 1,
"total": 0.41
}
},
"/path/to/your/project/lib/index.js:functionAsync": {
"async": {
"count": 3,
"total": 1.32
},
"sync": {
"count": 0,
"total": 0
}
}
}
node.profiler
还有一套报告器(目前只有一个报告器),它将输出一个漂亮的 HTML 文件,其中包含一个饼图。饼图有不同的视图,可以全面了解数据。
幕后
在尝试构建 node.profiler
时,解决了三个主要挑战。
- 钩入代码:要钩入各种代码,需要一个入口点。最适合的地方是 Node.js 的 `require` 函数,它是加载任何代码的入口点。一个 require 钩子被设计用来覆盖内置的 `require`,并在每次调用 `require` 时调用事件处理程序。它需要大量元数据才能为
node.profiler
提供强大的功能。例如调用文件、是否为原生、绝对路径(如果是文件)、是否仅在测试中需要、是否为第三方、是否为本地项目等。 - 一个 代理生成器(或包装器),它会发现我们从 `require` 钩子中获得的导出函数/对象,并根据需要正确地包装它们。
- 用相同的签名覆盖函数很棘手。JavaScript 内置的 `new Function` 允许您创建带有参数列表的新函数,但不支持闭包。因此,您无法创建带有参数(从被包装的函数复制)的函数。我最终保留了闭包函数的引用,并创建了与闭包兼容的新函数。 您可以在这里看到我的函数生成器。缺点是我必须保留所有这些函数的引用,在包装函数的情况下,这是正常且可预期的,我们已经保留了函数的引用。在回调函数的情况下,我们则不保留,因为回调函数最终会被垃圾回收。所以我们不使用函数生成器来包装回调函数。
高级用法
您可以直接使用 node.profiler
,也可以将其作为库使用。如果您将其作为库使用,则有一些额外的函数可以进行手动标记,以自定义任务计时。begin
/end
和 attachMonitor
是您需要使用的函数。您可以在 npm
注册表页面上找到它们。命令行工具有几个命令行参数,我将重点介绍最重要的几个。
verbose
:您可以设置两个级别的详细程度,以更深入地了解正在钩入什么以及正在调用什么等等。alternateProjectPaths
:这很重要,因为它允许您指定其他路径/子字符串,这些路径/子字符串将被视为“local
”,以便代码也钩入这些函数。默认情况下,它只会钩入项目本地的函数。includeThirdParty
:默认情况下为false
,将其设置为true
将基本上钩入所有内容!这可能会在某些代码上失败,但如果失败仅限于少数文件,您可以使用skipAttach
选项来控制要排除的内容。skipAttach
/includeAttach
:允许您精细调整要钩入或不钩入的“require”。- 如果您的项目中使用 grunt,您可以使用
runGrunt
来剖析 grunt 任务。 safeAsyncDetection
:默认值为true
,这会错过一些以async
模式运行的函数,但也能确保剖析代码不会崩溃。建议您尝试将其关闭,如果您的项目按预期工作,最好保持关闭,以正确检测异步函数。
开销
node.profile
工具会影响被监控应用程序的性能,但这通常是可以接受的,因为您不会在实时代码上使用剖析器。已经采取措施确保 node.profiler
所做的计算不包括它自身额外的时间,但由于嵌套函数调用,它无法做到 100% 准确。
我将在“幕后”部分讨论开销。
require 钩子相当耗费资源,因为对于每个 "require" 调用,它都会构建堆栈跟踪。它需要堆栈跟踪才能知道 require 是从哪个文件调用的,这是剖析器正确运行的关键信息。
同样,在每次 require 时,代理生成器都必须遍历结果并根据需要覆盖函数/构造函数。这需要时间,但这是每个文件一次完成的,因为已经钩入的函数不会再次被钩入。
前面两个点讨论的是影响目标应用程序代码的“加载”方面,而不是实际的有用的代码。然而,我接下来要讨论的最后一点,会影响目标应用程序的整体性能。
由函数生成器钩入的每个函数都会增加一个额外的间接层,并带有几个事件钩子。这些钩子相当快,进行简单的计算,但这会按函数调用执行,因此,总的来说,您会感觉到应用程序变慢了。很难说慢多少,但我对我在一些项目中的测试(端到端测试)进行了一些测试,时间差异约为 15-25%。这是我们为了获得目标应用程序的详细信息而必须付出的代价。
参考文献
历史
- 2014 年 12 月 4 日:初始版本