Jest内存泄漏问题

12
我正在对我的NodeJS API运行Jest单元测试和集成测试,但可能存在内存泄漏问题。我尝试将Jest从26.3.2升级到27.5.1,但效果不大。我从Chrome控制台中获取了一些堆快照。
快照1 enter image description here 快照2

快照3 在此输入图像描述

快照4

enter image description here

从上面的快照中,我可以看到使用量增加得非常高。但我无法理解出了什么问题。
我看到String、Object和JSBufferData有些问题,但不确定具体是什么问题。
在字符串的情况下,我看到这个: enter image description here 对于库的字符串化版本,有多个调用/行,但这来自何处以及为什么?
在对象的情况下:

enter image description here

这个截图中的对象可能来自我使用的库countries-list,用于获取国家列表以查找ISO名称。
最后是JSBufferData,它指向类似于URLSearchParam的东西,但我在我的应用程序中没有使用上述任何对象/库: enter image description here 我使用的堆栈: NodeJS: 16.14.2 Jest: 27.5.1 jest-serial-runner: 1.2.0

1
你是在哪里以及如何加载你的对象?看起来你正在使用xlsx包;你是在为测试加载文件吗?文件引用是否跨测试被保留? - Babak Naffas
1
@uday8486,你最终找到问题的原因了吗?我在我的堆中也看到了同样的情况,同样的模块一遍又一遍地被加载。 - Uri Klar
2个回答

17

我和我的团队在使用jest 29.x和node 18.x时遇到了内存泄漏问题。测试套件会运行,出现内存泄漏,并在内存使用量达到2GB时导致node崩溃。增加分配给node的RAM内存不是一个选项,因为测试套件的测试数量会增加,最终导致失败,而且2GB已经太多了。

实际解决方案

这个答案并不是直接解决问题的方法,但它可以将问题潜在地变得无害,并且我们花费了很多时间才找到它。

你需要做的第一件事是在你的jest.config.js文件中设置以下配置:

module.exports = {
  ...
  workerIdleMemoryLimit: '512MB',
  ...
}

我选择了512MB,但是你可以根据需要选择一个留有一些空间的值,比如"test_suite_ram_startup + 10 * memory_leak_increase"。当内存使用量达到这个值时,jest会重新启动工作进程并清除内存,对测试运行所需的总时间影响微乎其微。
仅凭这一点就足以防止测试套件崩溃,问题基本上已经解决了。
降低RAM总使用量
现在,与不让Node崩溃一样重要的是使用合理的RAM量来运行测试。从现在开始,每个考虑因素都会减少测试套件完整运行时所使用的RAM总量。
1. 使用以下配置运行jest:--expose-gc --no-compilation-cache:
node --expose-gc --no-compilation-cache ./node_modules/jest-cli/bin/jest.js --logHeapUsage

"logHeapUsage"会记录内存使用情况,以便您可以检查改进情况。
2. 如果您正在使用TypeScript,请使用SWC作为转译器将其转译为JS,而不是使用tsjest。这样做速度更快,节省了大量内存,因为转译过程是我们最耗费内存的过程。
3. 在测试套件的所有测试完成后,强制调用垃圾回收器。
afterAll(() => {
  global.gc && global.gc()
})

原则上来说,这不应该是必要的,但不知何故 jest 并没有像它应该做的那样进行得那么多。使用此方法,我们减少了每个测试套件执行时所需的总内存量。

4. 找到带有 import * as ... 的库

一些库(如 aws-sdk v2)会尝试导入所有内容,即使你只使用了其中一小部分代码。如果你查看库中导入发生的位置,你会发现所有内容都被导入到一个文件中,然后你从该文件中导入,而所有无用的东西都会存在于内存中。要解决这个问题,只需追踪你想要的资源确切位置,并直接从那里导入。在 aws-sdk 的情况下,你还可以使用 aws-sdk v3 来解决这个问题。

5. 使用小型库。

有时候我们不能只是替换库,但是将 class-validator 替换为像 zod 这样的小型库会有所帮助。

6. 更新你的库。

在我们的案例中,将 Prisma ORM 从 4.10 更新到 5.0 取得了巨大的效果。新版 Prisma 使我们的测试套件运行速度提高了 2.6 倍,并且占用的内存更少。

最终结果

只是为了让您对一些数字有所了解。我们的测试套件在启动时需要1.1GB的RAM,每个测试套件都会产生内存泄漏,增加总内存量100MB。每个测试套件运行需要15-10秒。执行10个测试后,节点将崩溃。
  • 实施1可以轻微减少总RAM量。
  • 实施4将启动内存使用降至400MB。
  • 实施3将内存泄漏增加降至每个测试套件80MB。
  • 实施2将启动内存降至30MB,并将内存泄漏降至每个测试套件60MB。
  • 我没有测量或记住5。
  • 实施6将内存泄漏增加降至每个测试套件14MB。

最终,每个测试套件运行时间为2-3秒。

我们从一个崩溃的测试运行开始,每个测试套件的RAM使用量从1.1GB逐渐增加100MB,到最后变成了一个从未崩溃的版本(这要归功于我在jest配置中提出的解决方案),每个测试套件的RAM使用量从30MB开始逐渐增加14MB。如果达到了512MB,它会立即降低到30MB,并再次缓慢增加。而且一切都变得更快了。
我真心希望这对那些遇到类似问题的人有所帮助。

1
workerIdleMemoryLimit 选项添加到我的 jest 配置中对我没有起作用,但是将其作为命令行参数添加就可以了(以防其他人在使用 jest 配置时遇到问题)。--workerIdleMemoryLimit=512MB这将我们的 CI 内存使用量从每次运行的 60GB 减少到 5GB。 - Aaron Wilson
1
非常感谢Pedro为这篇精彩的写作。 - undefined
非常感谢你的回复,Pedro。它帮助我解决了很多问题。然而,我还无法添加swc(@swc/jest)。我遇到了一个与“SyntaxError: Cannot use import statement outside a module”相关的问题,我通过配置对象“@swc/jest”解决了这个问题:jsc: { target: 'es2020', parser: { syntax: 'typescript', tsx: true, decorators: true, } }但是,我的组件的templateUrl和styleUrls出现了第二个错误,似乎swc无法解析它们。 - undefined

5
你可以尝试使用 --logHeapUsage 命令
来自 文档

记录每次测试后的堆使用情况。有助于调试内存泄漏。 在 Node 中与 --runInBand 和 --expose-gc 一起使用。

你可以尝试使用 --expose-gc 暴露垃圾回收器并添加

afterAll(() => {
  global.gc && global.gc()
})

另一种选择是使用jest -w 1来避免这些内存问题。
--maxWorkers
别名:-w。指定工作池将为运行测试而生成的最大工作程序数。在单次运行模式下,默认值为您机器上可用的核心数减去主线程的一个。在观察模式下,默认值为您机器上可用核心数的一半,以确保Jest不会干扰并使您的机器停滞不前。在资源有限的环境中(如CIs),调整此设置可能很有用,但默认值对于大多数用例应该足够了。
对于具有可变CPU的环境,可以使用基于百分比的配置:--maxWorkers=50%
参考文献:
无法找到Express.js Jest测试中的内存泄漏
我的Jest测试正在泄漏内存,我该怎么办?
在Jest中调试内存泄漏的步骤是什么?
https://chanind.github.io/javascript/2019/10/12/jest-tests-memory-leak.html

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接