prettierを速く動かしたくてparallel-prettierに.prettierignoreを読む修正を加えて動かして速くなったか


背景

prettier とは

prettier はソースコードを整形するフォーマッターです。
ごく少ない設定項目しか持たないため、事前準備をほとんどすることなく使い始めることができます。

prettierは複数のプログラミング言語を整形することができます。
今回はprettierでJavaScriptのソースコードをフォーマットします。

prettierの並列化

500ファイルのJavaScriptのソースコードをバンドルする際に

prettier --check

コマンドを実行して、フォーマット漏れが無いか確認しています。
実行すると3.33秒掛かかります。

~ time npx prettier --check 'src/lib/**/*.js'
All matched files use Prettier code style!

________________________________________________________
Executed in    3.33 secs   fish           external
   usr time    4.92 secs  125.00 micros    4.92 secs
   sys time    0.20 secs  875.00 micros    0.20 secs

これをもう少し速くしたいです。
prettierはファイル単位で、フォーマットをチェックします。
ファイル間の依存関係がないため、ファイル単位での並列化は容易なはずです。

現にparallel-prettierという、prettierを並列実行するラッパーコマンドが存在します。

今どきの開発用PCのCPUはマルチコアです。
依存関係のないタスクを並列化すれば、コア数が許す限り理想値に近い性能向上が見込めそうです。
4コアのCPUで4並列化すれば、4倍の早さ、0.8秒でチェックが完了するのではないでしょうか?

解決したい課題

parallel-prettierの性能は?

さっそくparallel-prettierを使ってみましょう。

~ time npx pprettier --check 'src/lib/**/*.js'
bundle.js
modules/jquery.jsPlumb-1.5.5-min.js
modules/jquery.jsPlumb-1.5.5.js
✖ 3 files were not formatted

________________________________________________________
Executed in    9.18 secs   fish           external
   usr time  488.60 millis  126.00 micros  488.48 millis
   sys time  107.34 millis  903.00 micros  106.44 millis

実行時間が9秒に伸びました!?

parallel-prettierは.prettierignoreを無視

原因はparallel-prettierが.prettierignoreを無視しているためです。

対象ディレクトリにはいくつかのバンドル済みライブラリが配置されています。
それらのファイルは.prettierignoreファイルに記載して、prettierの対象から外しています。

.prettierignoreを削除して、prettierを実行すると

~ time npx prettier --check 'src/lib/**/*.js'
Checking formatting...
[warn] src/lib/bundle.js
[warn] src/lib/modules/jquery.jsPlumb-1.5.5.js
[warn] Code style issues found in the above file(s). Forgot to run Prettier?

________________________________________________________
Executed in    9.02 secs   fish           external
   usr time   12.89 secs  111.00 micros   12.89 secs
   sys time    0.50 secs  834.00 micros    0.50 secs

実行時間は9秒に伸び、チェックに失敗します。
parallel-prettierとほぼ同じ結果になります。

これらのライブラリをparallel-prettierのチェック対象から除外しなくては、高速化できません。
また、チェックに失敗するので、本来の目的であるフォーマット済みの検証ができなくなります。

parallel-prettierが.prettierignoreを無視するのを解決する

prettierのAPIにはprettier.getFileInfoがあります。
これを使うと.prettierignoreファイルに記載されているかどうか判定できます。
次のように使います。

const { ignored } = await prettier.getFileInfo(file.path, {
  ignorePath: './.prettierignore',
});

これをparallel-prettierに組み込んだものが

feature: Do not format the files listed in .prettierignore by ledsun · Pull Request #6 · mixer/parallel-prettier

です。

結果

上記ブランチをチェックアウトしてnpm run buildを実行するとdist/src/index.jsができます。

~ time ~/parallel-prettier/dist/src/index.js --check 'src/lib/**/*.js'
✔ Checked 554 files

________________________________________________________
Executed in    2.67 secs   fish           external
   usr time  422.47 millis  133.00 micros  422.34 millis
   sys time   85.01 millis  869.00 micros   84.14 millis

2.67秒に縮まりました。やったね1.25倍速くなりました!
「えーっ、2倍にもならないのかよ・・・」がっかりです。

参考に、prettierignoreに記載しているライブラリを削除して、本家parallel-prettierを使ってみます。

~ time npx pprettier --check 'src/lib/**/*.js'
✔ Checked 551 files

________________________________________________________
Executed in    2.81 secs   fish           external
   usr time  412.89 millis  127.00 micros  412.76 millis
   sys time   84.67 millis  962.00 micros   83.70 millis

2.81秒です。prettierignoreを参照したことで遅くなったわけではありません。
今回のファイル群は、並列化してもあまり速くならないようです。

考察

今回の事例ではparallel-prettier並列化してもあまり速くなりませんでした。
並列化して速くならないことは、よくあることです。

しかし、parallel-prettierのREADMEには、16〜22倍高速化されたと書いてあります。

これは納得いきません。
原因の候補を考えてみます。

prettierのフォーマット処理はCPUバウンドでない?

フォーマット対象ファイルの読み込みがボトルネック?

parallel-prettierはファイル読み込みもワーカーに分散して、並列化しています。PCのIOの限界に到達するまで高速化するはず。

PCのIOが限界に到達するか調べる方法がわかりません。

メモリ不足で仮想ディスクを使っている?

メモリ余ってます。

ファイル単位の並列化ではボトルネックが解消されない?

特定のファイルのフォーマットがボトルネック?

否定する決め手はありません。
ファイル単位のフォーマット実行速度を計測すると良さそうに思います。

ファイルのフォーマット処理より、ファイル収集処理がボトルネック?

printfデバッグした感じ、フォーマット処理が始まるまでに秒単位の時間はかかっていません。

prettierが十分高速?

prettierはすでに、並列化されていて、さらに並列化しても速くならない?

現象としては魅的な理由です。

例えば、prettier実行時、nodeプロセスのCPU使用率が100%を超えています。

もっともらしく思えます。
単に、Node.js自体は並列化され(マルチスレッドで動い)ているからです。

Feature Request: Parallel/Clustered Prettier · Issue #4980 · prettier/prettierがopen中なのと矛盾しています。

また、prettierのソースコードを検索しても、clusterchild_processspawnworker-threadcpuなどのそれらしい単語は含まれていません。
並列化しているとは思えません。

parallel-prettierの使っているprettierのバージョンが古い?

最新バージョン2.2.1を使ってもparallel-prettierの実行時間に変化はありませんでした。

マルチプロセスによる並列化ではV8のJITコンパイラの効きが悪い?

  • parallel-prettierのREADMEに載っているスコアと矛盾しています。
  • Node.jsのCluster APIの存在とも矛盾します。

過去の経験上からもなさそうな気はします。

sosukesuzuki/prettier-parallel: Runs Prettier with Worker Threads はWorker Threadを使ってprettierを並列化しています。
parallel-prettierとprettier-parallelの性能を比較すると何かわかるかもしれません。
prettier-parallelはフォーマット対象のファイルが決め打ちです。
試すには何かしら修正が必要です。

オチ

実のところwebpackの実行に8秒かかっているため、prettierの3秒は、バンドル手順全体からみるとボトルネックではありません。