ishikawa_pro's memorandum

若手webエンジニアの備忘録です.

Swift Node.js Docker AWS etc...色々やります。

jestのテストが遅い場合に確認すべきこと

こんにちは。
急に寒くなりましたね。
僕は最近iPad Air4 を予約しました。

今日は、javascriptのテストフレームワークのjestについてです。
業務では去年くらいから新規サーバーアプリケーションはjavascriptではなくTypeScriptを使っています。
それに合わせてテストフレームワークもmochaからjestへ移行しました。

jestjs.io

jestの特徴の1つは、一意なグローバル状態を持つことを保証しつつ、複数のテストを別プロセスで並列に実行してくれるため、安全に高速なテストをすることが可能なことです。
しかし、僕が前にいたチームでjestのテストにかかる時間がやたら長いことが話題になってました。
今日は、この高速なはずのjestがやたら遅い問題が社内で話題になっていたので調査してまとめてみたので、もしjestが遅くて困っている方は参考にしてみてください。

現状の問題

テストは基本的に、開発者のローカルPC(mac)でコンテナなどは使わずに走らせているのと、gitのサブブランチにpushした際などにgoogle cloud build を使ったコンテナ上のテストの2箇所で走っています。
そのローカルとCIどちらとも非常に遅い状態でした。
今回はこの2つの環境での問題について調査しました。

macでjestが遅い問題

まずは、mac上で遅い問題について調査しました。
実際、僕がいた時も若干遅さを感じていましたが、それからさらにコードとテストが増えていき、テストにかかる時間も比例して長くなっていったようです。
現状、51個のTest Suiteと285個のTestがあります。
タイムは、npx jest --clearCache でキャッシュを消した状態で実行すると約69秒でした。

Test Suites: 51 passed, 51 total
Tests:       285 passed, 285 total
Snapshots:   0 total
Time:        69.496s

同じコードでは無いので厳密な比較にはなりませんが、Node.jsとmochaで書かれた別のアプリケーションでは、テストの数が1000個を超えてますが30秒くらいしかテストの実行にかかりません。
それに比べるとテストの数が3分の1しかないのに速度が倍以上遅いです。

pprofでプロファイル

最近、node.jsでもpprofが使えるということを知って、プロファイルしてみるのがマイブームなのでとりあえずpprofを試してみました。
参考記事
moznion.hatenadiary.com

会社のコードのため、セキュリティとか色々な関係でプロファイルしたものは載せませんが、testごとの初期化がmaxで2~3秒かかっているものはあったものの、テスト1つ1つに大きな問題はなさそうでした。
メモリもそれぞれのテストでは特別多く使ってる感じはなかったです。

htopでCPUなどの使用率をみる

次にhtopコマンドで、テスト実行中のCPUやメモリーの使用率を確認しました。
ちなみに会社で支給されているマシンはMacBook Pro (15-inch, 2017) で、クアッドコアIntel Core i7です。(近いうちに新しいモデルにする予定)
こちらがテスト実行中のhtopの画面。
f:id:ishikawa_pro:20201018182058p:plain あんまりCPUとか詳しくないですが、hyper threading により物理コアは4つですが、PCからは論理プロセッサを含めて8つのコアがあるように見えているので、htopでの使用率も8個表示されています。
そして物理コア分しか使用率が振り切っていないことがわかります。

続いてこれは、jestで実行中のtestの数です。
f:id:ishikawa_pro:20201018182933p:plain
7つ同時にテストを走らせていることがわかります。
コードを確認したところ、ここで最大worker数を決めているようです。
https://github.com/facebook/jest/blob/a1adaff21db93517e60be840303518490cd5d51f/packages/jest-config/src/get_max_workers.js#L20

const cpus = os.cpus().length;  // 8
return Math.max(argv.watch ? Math.floor(cpus / 2) : cpus - 1, 1);

しかしhtopを見る限り論理プロセッサは全く使われていない様子なので、4プロセッサで7つworkerを管理している状態でした。
そこで、最大worker数を4つに制限して再度テストを実行してみました。
worker数は --maxWorkers オプションで指定できます。

Test Suites: 51 passed, 51 total
Tests:       285 passed, 285 total
Snapshots:   0 total
Time:        60.062s

結果10秒早くなりました。(キャッシュをクリアした状態)
キャッシュが効いた状態のテストだと30 ~ 40秒くらいになったので、まあ今までよりはストレスフリーになったでしょうという感じです。

考察

結局mochaを使ってるプロジェクトの方が速度が早いですが、mochaで書かれたテストはテストごとに状態が一意ではなく、globalに1度セットアップした状態を共有してるので、最初の初期化が終わってしまえばテストが直列実行でも高速です。(書き方にもよると思いますが)
しかしjestはそれぞれのテストが独立していて、一意なグローバル状態を持つことが保証されていることが売りなため、テスト毎に初期化があってそれに時間がかかってしまうことはしょうがない気がします。
なので、それぞれが分離されて独立していることによる、より正確なテストとのトレードオフなのかなと思いました。

CI上ではさらに遅い問題

一旦ローカルでの速度問題は解決したということで、次にCI上での問題についてです。
CIはgoogle cloud buildを使っていて、コンテナ上でjestが実行されています。
一番遅かった時期が、ちょっと前の話だったのでログがどこにあるか分からなかったのですが、7分くらいはかかっていたそうです。
とりあえずCIと同じよな環境で実行すべく、ローカルにコンテナを立ち上げてテストを実行してみました。
実行中のhtopの画面がこちら。
f:id:ishikawa_pro:20201018193536p:plain
コアが全部振り切ってて、良さそうに見えてましたが、いつまで経っても一向にテストが進まないので途中で諦めてしまいました。
コンテナの使えるCPU数を減らしたりなど色々試しましたが遅いままでした。
おそらくcloud build上でも同じことが起こってるんだろうと思います。
それで、ググってみたところ公式ドキュメントのトラブルシューティングに、DockerやCIサーバー上でjestが遅い場合の対処法が書いてありました。

jestjs.io

どうやら、高速SSDとマルチコアCPUを搭載した今時のPCなら高速だけど、特定のセットアップでは低速になる場合があるようです。
このドキュメントにリンクされているissueにも、circleCIやTravisCIでテストを実行するとlocal pcに比べて遅いことなどが報告されてました。

github.com

対応方法としては、 --runInBand オプションを付けて直列にテストを実行させると早くなるらしいです。
なんと最大50%早くなったとの報告も。

実際にローカルのコンテナでも直列実行したところ、並列だといつまで経っても1個もテストが進まなかったのに対して、直列だとちょっと遅いですが確実に進んでました。
時間も3分ちょっとくらいにはなりました。

Test Suites: 51 passed, 51 total
Tests:       285 passed, 285 total
Snapshots:   0 total
Time:        202.734s

考察

traviceCIなどのVMベースのCIだと、CPUのコア数をみて、適切な worker数を指定してあげれば高速になるそう。
それでも解決しない場合は、 --runInBnad で直列に実行させるしかなさそうです。
あとは、ちょっと微妙ですが、テストをディレクトリ毎に分けて実行するようにして、cloudbuild上でディレクトリごとのテストを別ステップとしてパラレルに行うとかすれば早くなるかもなと思いました。

まとめ

今回は、ローカルとCI上でテストが遅かったので、原因を探るために色々調査しました。
ローカルの方は、ちょっとパフォーマンス改善できたのでよかったです。
正直CI上で遅い時は、直列実行するしかないってのは、jestの旨味が半減してるようで微妙だなとおもいました。
もっといい方法がないか、今後も調査してみます!