github.com
scalafmtはscalametaを利用したscalaのコードフォーマッタです。intellij plugin・sbt plugin・cli・mavenやgradle plugin といった様々な形で提供されており
と様々な方法で利用することができます。
scalafmtの課題の一つはscalafmtの実行にかかる時間です。機能が豊富で複雑なため、ただでさえそれなりに実行に時間がかかってしまうのですが、さらに悪いことにscalafmtの実行に当たってはjvm の起動がオーバーヘッドとなってきます。利用頻度が少ないのならば問題ないのですが、gofmtなどのような軽量なフォーマッタのように気軽に実行できないものだろうか。
このjvm起動のオーバーヘッドを解決する方法の一つがnailgunです。nailgunはJVMプロセス(サーバ)をあらかじめ起動しておいて、クライアント側からサーバー側にjavaプログラムの実行要求をし、サーバー上でプログラムを実行させることでjvmの起動によるオーバーヘッドを解決してくれます。scalafmtからの利用方法
github.com
他の解決方法としてはどうにかしてscalafmtをネイティブバイナリにAOTコンパイルしてしまうことですが、残念ながらv1.6.0-M4段階ではscalanativeサポートはされていません
Support Scala Native · Issue #1172 · scalameta/scalafmt · GitHub
しかしGraalVMのツールチェーンの一つであるsubstratevm(とgraal)を使うことでscalafmtをAOTコンパイルしnailgunなしに爆速で実行することができるようになります。
SubstrateVM
ただしJavaアプリケーションをAOTコンパイルしてしまうので、(特にdynamic class loadingまわりなど)いくつかの制約がある graal/LIMITATIONS.md at master · oracle/graal · GitHub
scalafmtをAOTコンパイルする
先日scalafmtに上記の制約をパスさせAOTコンパイルを可能にするPRが来ていた!のでこれでscalafmtをネイティブイメージにコンパイルできるぞ!
github.com
ということでビルドしてみる
$ sbt cli/assembly
$ native-image -jar scalafmt-cli/target/scala-2.12/scalafmt.jar
いろいろオプションつけたりはできるけどこれでネイティブイメージがビルドできた。楽すぎる...
パフォーマンスの計測
実行環境
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"
$ java -version
openjdk version "1.8.0_172"
OpenJDK Runtime Environment (build 1.8.0_172-20180625212755.graaluser.jdk8u-src-tar-g-b11)
GraalVM 1.0.0-rc5 (build 25.71-b01-internal-jvmci-0.46, mixed mode)
$ cat /proc/cpuinfo | grep "model name" | uniq
model name : Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz
$ cat /proc/cpuinfo | grep "physical id" | uniq
physical id : 0
$ cat /proc/cpuinfo | grep processor | wc -l
8
$ cat /proc/cpuinfo | grep "cpu cores" | uniq
cpu cores : 4
パフォーマンスの計測には Merge pull request #1276 from scalameta/fix-quote · scalameta/scalafmt@5a5747f · GitHub でビルドした3種類のscalafmt-cliを利用しました。
scalafmt-jvm
- coursier bootstrapで固めた普通のCLI
ng-nailgun scalafmt
- この方法でビルドした nailgunを利用したscalafmt-cli
- ただしnailgunサーバーを温めるために予め一度実行しておき、その後計測環境を同じくした上で二回目の実行にかかった時間を実測値とする
scalafmt-native
- substratevmを利用して作ったscalafmtのネイティブイメージ
フォーマットの実行対象は2種類
scalafmtの設定は scalafmt/.scalafmt.conf at c92153e777984db6d69ec359dd1b1115bd2199d6 · scalameta/scalafmt · GitHub
scalameta/scalafmt全体
$ /usr/bin/time ./scalafmt-jvm
Reformatting...
100.0% [
37.98user 0.69system 0:06.11elapsed 632%CPU (0avgtext+0avgdata 1001288maxresident)k
0inputs+41984outputs (0major+250221minor)pagefaults 0swaps
$ /usr/bin/time ./scalafmt-native
Reformatting...
100.0% [
6.57user 0.17system 0:02.67elapsed 251%CPU (0avgtext+0avgdata 388632maxresident)k
0inputs+952outputs (0major+99109minor)pagefaults 0swaps
$ /usr/bin/time ng-nailgun scalafmt
Reformatting...
100.0% [
0.00user 0.00system 0:01.36elapsed 0%CPU (0avgtext+0avgdata 1796maxresident)k
0inputs+0outputs (0major+73minor)pagefaults 0swaps
単一ファイル
$ /usr/bin/time ./scalafmt-jvm scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala
12.38user 0.33system 0:02.85elapsed 445%CPU (0avgtext+0avgdata 363232maxresident)k
0inputs+41064outputs (0major+89218minor)pagefaults 0swaps
$ /usr/bin/time ./scalafmt-native scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala
0.13user 0.04system 0:00.17elapsed 100%CPU (0avgtext+0avgdata 181420maxresident)k
0inputs+32outputs (0major+41286minor)pagefaults 0swaps
$ /usr/bin/time ng-nailgun scalafmt scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala
0.00user 0.00system 0:00.16elapsed 1%CPU (0avgtext+0avgdata 1716maxresident)k
0inputs+0outputs (0major+70minor)pagefaults 0swaps
やはりnailgunによる実行が早いのは当然ですが、普通にJVMを起動するCLIの実行と比べるとめちゃくちゃ早くなっていることがわかります:tada:
特に単一ファイルに対するフォーマットの実行のようなshort-live(プロジェクト全体のフォーマットと比べて)な実行の場合はnailgunを利用した場合の実行時間にかなり迫っています。
この実行速度(かつnailgun不要)ならエディタ保存時にscalafmt実行とかしても快適に過ごすことができるのではないでしょうか
(もっともプロジェクトに関わる全員のscalafmtのバージョンを揃えないとフォーマット結果がバラバラになりうるのであまり推奨はできない気がする)
所感
scala-nativeが出たときにscala->llvm ir->nativeよりも、scala->jvm bytecode->nativeでネイティブイメージ作れたほうが、scala-nativeが頑張ってるような再実装の嵐を免れることができてお得だよなぁ、実際にはいろいろ制約あって難しいのかなぁと考えていたのですができてしまったよ...
サーバープログラムのようなlong-liveなプロセスに対してはHotSpotVMなどのJITの恩恵が大きそうですが、フォーマッタやlinterやその他CLIツールのようなshort-liveなプロセスにとっては非常に嬉しいツールですね、ありがとうoracle。仕組みがいまいち追いきれてない(論文もsubstratevmに関してはどれを読めばよいのか)なので頑張ってコード読むなりして勉強していきたい。
参考資料