💧

【Jest】heap out of memoryの対処

に公開

はじめに

ある日突然、Jestを使ったテストがヒープメモリ不足によるエラーで失敗するようになりました。
その時に調べたことや対処法を忘備録的に残します。

環境:

  • Jest 29.7.0
  • Node.js 20.19.0

ログ:

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
ログ全体
<--- Last few GCs --->

[30094:0x7ff78a600000]    20493 ms: Mark-Compact (reduce) 4144.9 (4148.3) -> 4091.3 (4095.0) MB, 36.04 / 0.00 ms  (average mu = 0.826, current mu = 0.735) allocation failure; scavenge might not succeed
[30094:0x7ff78a600000]    20612 ms: Mark-Compact (reduce) 4144.0 (4147.6) -> 4143.9 (4147.6) MB, 26.43 / 0.00 ms  (average mu = 0.807, current mu = 0.778) allocation failure; scavenge might not succeed


<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

 1: 0x109d9a870 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/usr/bin/node]
 2: 0x109f5a83c v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/usr/bin/node]
 3: 0x10a14a5d7 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/usr/bin/node]
 4: 0x10a148e39 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/bin/node]
 5: 0x10a13d6a1 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/bin/node]
 6: 0x10a13e0e5 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/bin/node]
 7: 0x10a11fa3c v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/usr/bin/node]
 8: 0x10a115a86 v8::internal::MaybeHandle<v8::internal::SeqTwoByteString> v8::internal::FactoryBase<v8::internal::Factory>::NewRawStringWithMap<v8::internal::SeqTwoByteString>(int, v8::internal::Map, v8::internal::AllocationType) [/usr/bin/node]
 9: 0x10a5b7f6d v8::internal::Runtime_StringBuilderConcat(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]
10: 0x10a96f376 Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit [/usr/bin/node]
Abort trap: 6

結論

以下の設定をjest.config.tsに追加することで、メモリエラーを回避できました。

jest.config.ts
  workerIdleMemoryLimit: "1GB", // ワーカーが使用できるメモリ上限を制限
  maxWorkers: 2, // ワーカーの最大数を2に制限

なお、根本原因としては、Node v16.11.0以降に利用可能なRAMをすべて消費してしまう問題があるようです。
この問題はNode v21.1.0で修正済みで、またv20.10.0,18.20.0にも修正が適用されています。
https://github.com/jestjs/jest/issues/11956

そのため、Nodeのバージョンを更新するのも解決策の1つ ですが、対応当時は LTS 版がリリース前だったため、workerIdleMemoryLimit での回避策を採用しました。

2025年2月にはNodeのLTS版 v22.14.0がリリースされているので、バージョンを上げれるのであれば一番良い選択肢かなと思います。(本記事では動作未検証です)
https://nodejs.org/ja/about/previous-releases#各バージョンの最新のリリース

調べたこと

JavaScript heap out of memory

ある日突然、Jestを実行するとテストが失敗するようになりました。

ログを見ると

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

とあり、ヒープメモリ不足により失敗していることが分かります。

どうやら、16.11.0以降のNodeには、利用可能なRAMをすべて消費するというバグがあるようです(!)

We had some issues with Jest workers consuming all available RAM both on CI machine and locally.
https://github.com/jestjs/jest/issues/11956

Node 21.1.0,20.10.0,18.20.0には修正適用済みのようですが、ダウングレードは避けたかったのと、対応当時は修正適用されたLTS版がまだリリース前でしたので、issueに記載されているworkerIdleMemoryLimitオプションを使うことにしました。

If you're unable to upgrade your version of Node, you can use --workerIdleMemoryLimit in Jest 29 and later.
https://github.com/jestjs/jest/issues/11956

jest.setup.tsworkerIdleMemoryLimitを追加します。

jest.setup.ts
+ workerIdleMemoryLimit: "1GB",
maxWorkers: 1,

これにより、ワーカーの使用メモリが指定上限に達するとワーカーが再起動されるようになります。
つまりは、ワーカーが使用できるメモリの上限を制限しています。

今回は容量で指定しましたが、%指定もできるみたいです。
https://jestjs.io/ja/docs/next/configuration#workeridlememorylimit-numberstring

これで解消!と思ったのですがそうはいかず、、
変わらずメモリエラーが発生してしまいました。

workerIdleMemoryLimit のバグ

今度はJest側に問題があるようでした。
どうやら、Jestのワーカーが1のときにworkerIdleMemoryLimitでメモリ上限を指定してもワーカーの再起動がされない問題があるようです。
https://github.com/jestjs/jest/issues/13792

ワーカー数は、maxWorkersオプションで指定することができます。
https://jestjs.io/ja/docs/next/configuration#maxworkers-number--string

例えば1とした場合は1ワーカーでテストが実行されるため、すべてのテストが直列実行されます。
2とした場合は2ワーカーでテストが実行される、つまり2テストスイートが並列実行されます。
また、指定なしの場合は、メインスレッド用の1コアのみ残して他の利用可能なコアはすべて利用して並列実行されます。

もともとのJestの設定では、テストを直列実行するためにmaxWorkers: 1を指定していました。
そのため、workerIdleMemoryLimitを設定していてもメモリを超えたワーカーがそのまま残り続け、結局メモリエラーが発生してしまいました。

このバグはJest v29.4.3で修正されたとの記載がありますが、

[jest-core] allow to use workerIdleMemoryLimit with only 1 worker or runInBand option (#13846)
https://github.com/jestjs/jest/releases/tag/v29.4.3

実際にはv29.7.0で解消が確認できなかったため、maxWorkersの値を修正することにしました。

jest.setup.ts
+ maxWorkers: 2,
- maxWorkers: 1,
workerIdleMemoryLimit: "1GB",

直列実行から並列実行に変更したため、DBアクセスの競合が発生しないようにテストデータを調整する必要がありましたが、これによりメモリエラーが発生しなくなりました🥳

おわりに

Jestのメモリエラーの対処法と、調査した内容の忘備録でした。

今回の対策で一応の解決はしましたが、Node.js v22もリリースされたことですし、DBの競合を考えながらテストを並列実行するよりNodeのバージョンを上げることが最善の対応かなと思います。

とはいえ、すぐにバージョンアップできない場合でもworkerIdleMemoryLimitの設定は有効でしたので、同じ問題にあたった際は試してみてください!

最後まで読んでいただきありがとうございました。

Discussion