Jest のメモリリークとその対処

現在、Jest にはメモリリークの問題がある。テストが増えてきたことで、テストを実行するとマシンのメモリを食い尽くすようになってきた。その調査と対処の記録。

2024/04/08追記
問題は Node.js の方にあったようで、Node.js 21.1 で修正されたようです。
https://github.com/jestjs/jest/issues/7874#issuecomment-1935517499

# TL;DR
- Jest v29 以降ならば、この問題を回避するための設定 [workerIdleMemoryLimit](https://jestjs.io/docs/next/configuration#workeridlememorylimit-numberstring) を使える
- Create React App に bundle されている Jest はバージョンが古いので、workerIdleMemoryLimit は使えない。その場合、環境変数 `NODE_OPTIONS=--max-old-space-size=<MB>` を指定してヒープの最大容量を制限することで回避可能
- 大体全部ここに書いてある
https://github.com/jestjs/jest/issues/11956#issuecomment-969041221

# 再現

テストプロジェクト作成。
```bash
#! /bin/env bash
set -eu
PJ=mem-leak-test
TEST_DIR=$PJ/src/tests
npx create-react-app $PJ
mkdir $TEST_DIR
for i in {1..100}; do
  echo "test('t', () => expect('A').toBe('A'))" > `printf $TEST_DIR/%03d.test.jsx $i`
done
```

Jest の [--logHeapUsage](https://jestjs.io/ja/docs/next/cli#--logheapusage) を使って計測。--expose-gc を使えとあるが、[環境変数 NODE_OPTIONS 経由では指定できない](https://nodejs.org/api/cli.html#node_optionsoptions)ので、`npm run test` ではなく内部スクリプトを直接実行。

```bash
node --expose-gc node_modules/react-scripts/scripts/test.js --no-watch --runInBand --logHeapUsage src
```

結果。
```
PASS src/App.test.js (53 MB heap size)
PASS src/tests/012.test.jsx (56 MB heap size)
PASS src/tests/094.test.jsx (59 MB heap size)
PASS src/tests/098.test.jsx (62 MB heap size)
PASS src/tests/043.test.jsx (65 MB heap size)
~~ 中略 ~~
PASS src/tests/008.test.jsx (313 MB heap size)
PASS src/tests/099.test.jsx (332 MB heap size)
PASS src/tests/056.test.jsx (335 MB heap size)
PASS src/tests/006.test.jsx (338 MB heap size)
PASS src/tests/051.test.jsx (343 MB heap size)
PASS src/tests/060.test.jsx (344 MB heap size)
PASS src/tests/052.test.jsx (347 MB heap size)
PASS src/tests/003.test.jsx (350 MB heap size)
```

# 対処

このメモリリークの問題は下記の通り Issue として登録されているが、まだ解決には至っていない。

https://github.com/jestjs/jest/issues/11956#issuecomment-969041221

v29 から回避策として workerIdleMemoryLimit という設定が追加されているが、我々は Create React App を使っている都合上、それは使えないので、代わりに Node.js のオプション [--max-old-space-size](https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-megabytes) を使って対処。

試しに、150MB に制限して実行してみる。

```bash
node --expose-gc --max-old-space-size=150 node_modules/react-scripts/scripts/test.js --no-watch --runInBand --logHeapUsage src
```

結果。

```
PASS src/tests/040.test.jsx (45 MB heap size)
PASS src/tests/016.test.jsx (49 MB heap size)
PASS src/tests/082.test.jsx (52 MB heap size)
PASS src/tests/094.test.jsx (56 MB heap size)
PASS src/tests/021.test.jsx (97 MB heap size)
PASS src/tests/013.test.jsx (102 MB heap size)
PASS src/tests/025.test.jsx (103 MB heap size)
PASS src/tests/004.test.jsx (106 MB heap size)
~~ 中略 ~~
PASS src/tests/100.test.jsx (100 MB heap size)
PASS src/tests/090.test.jsx (103 MB heap size)
PASS src/tests/048.test.jsx (107 MB heap size)
PASS src/tests/057.test.jsx (109 MB heap size)
PASS src/tests/097.test.jsx (103 MB heap size)
PASS src/tests/059.test.jsx (105 MB heap size)
PASS src/tests/099.test.jsx (108 MB heap size)
PASS src/tests/058.test.jsx (111 MB heap size)
PASS src/tests/006.test.jsx (114 MB heap size)
PASS src/tests/003.test.jsx (116 MB heap size)
PASS src/tests/088.test.jsx (117 MB heap size)
PASS src/tests/070.test.jsx (68 MB heap size)
PASS src/tests/072.test.jsx (71 MB heap size)
```

150MB を境にメモリが解放されていることが見て取れる。

普段は、次のように環境変数 NODE_OPTIONS 経由で指定すると楽。

```bash
NODE_OPTIONS=--max-old-space-size=150 npm run test
```


以上。