.NET Core Cliを20倍高速に自前ビルドするtips

2016年8月28日日曜日

こんにちは、皆さん今日も.NET Core Cliをビルドしていますか?
私は昨日ひさびさに.NET Core Cliをビルドしてみたところ、割とビルド時間の長さが苦痛でした。もう少しスナックに.NET Core Cliへ手を入れるために、いくつかビルド時間を短縮するための方法を考えたのでメモしておきます。

そもそもビルドに失敗する系

Windows: Quartus Primeをインストールしている場合

どうもおかしいなぁと思ってパス解決部分を調べていったところ、まさかのAltera社Quartus Prime関連(正確にはModelSim for Altera Lite)でエラーを吐いていることに気付きました。そんなばかな。
文字列ダンプは図1の通りです。
図1 AlteraのQuartus Primeのパス設定に謎の改行コード群が含まれる
環境変数設定ダイアログを使って慎重にパスを編集しましょう。

Windows: NFTSのジャンクション利用に注意

私の作業用PCでは、SSD容量の都合からC:\Users\muo\workspaceE:\workspaceへジャンクションによってマッピングしています。
Windows版のGitクライアントは、一部のサブコマンドにおいてジャンクションをうまく参照できません。
このため、.NET Core Cliビルド序盤でgit "rev-list" "--count" "HEAD"が実行されるポイントで
fatal: Unable to read current working directory: No error
というエラーを吐いて止まってしまいます。
ジャンクションで参照されている側のドライブ/ディレクトリへ移動してビルドを実行しましょう。

ビルドが遅い系

公式の開発ガイドに従ってビルドをおこないます。通常はbuild.cmdまたはbuild.shを叩くことになります。
Windowsでのビルドについて、あまりPowerShellスクリプトのinvokeを使いたくないので私は
build_projects/dotnet-cli-build/build.ps1
を直接実行しています。

ビルドが実際遅い

Gitからcloneしてきたままのそのままの状態のビルドは結構遅いです。
MacBookでは57分ぐらい(56分41秒)かかります。MacBook(2015)でやることではないと思いますが。

tip: ビルド冒頭でのNuGetパッケージ削除をやめる

2回ビルドして所要時間がほとんど変わらなかったため、毎回クリーンビルドが走っているのではという疑いを持ちました。特に、ビルドの序盤ステージで大量のパッケージのdotnet restoreをおこなう部分で毎回数分間かかっていました。
ビルドスクリプトをざっと確認したところ、https://github.com/dotnet/cli/blob/rel/1.0.0/build_projects/dotnet-cli-build/build.sh#L103-L106できっちり削除しています。Windows版はhttps://github.com/dotnet/cli/blob/rel/1.0.0/build_projects/dotnet-cli-build/build.ps1#L82-L83でクリアしています。
これらをコメントアウトしてローカルの生成物を維持すれば、ほとんどコード変更が無い場合のビルド時間を短縮できそうです。試したところ、うまくいきました。
しかしMacBook上で52分程度(52分8秒)と、4.5分ぐらいしか縮んでいません。

tip: ビルド後半のテスト工程をごっそり削る

.NET Core Cliのビルド後段はほとんどテスト実行なので、試行錯誤フェーズでは不要と切り捨てることにしました。
build.shには--targets Prepare,Compileを渡し、build.ps1には-Targets Prepare,Compileを渡すことでテストフェーズを飛ばせます。
この結果、MacBookではビルドが12分半(12分36秒)で完了するようになりました。依然として結構な時間がかかっていますが、最初から比べると5倍近いのでよしとしましょう(大きめのAndroidアプリをビルドするのにかかる時間とあまり変わらない)か。きっと、まともなCPUを積んだMacであれば5分そこらで終わるのでしょう、めでたしめでたし。

適当なWindows PCでのビルド時間が案外長い

さすがに常用環境としてMacBookでビルドするのは厳しいので、普段はWindows上で開発をおこないます。
MacBookで12分ということはWindows PCでは4-5分ぐらいだろうと考えました。しかしWindows上でのビルド時間を計測したところ、Core i5-4460を搭載したPCにて前述の2つの対応をおこなった状態で7分程度でした。
このCPUは4コア積んでいて、その割にはMacBookと時間差があまりついていないのが気になります。
ビルド中にCPU使用率の状態を簡単に確認していた限りではほぼ1コアしか使われていないようでした。ビルドシステム自体がパラレルビルドをサポートしていないこと、シリアルのビルドでコアを複数利用できていないという事情がありそうでした。
あと、特に時間を食っているのがLZMAへの圧縮工程です。
publish: Published to E:\workspace\cli\artifacts\win10-x64\tools
Published 1/1 projects successfully
[EXEC       <] [ OK ] [00:01:32.596] E:\workspace\cli\artifacts\win10-x64\stage1\dotnet.exe "publish" "--output" "E:\wor
kspace\cli\artifacts\win10-x64\tools" "--configuration" "Debug" exited with 0
[EXEC       >] [....] [00:01:32.597] E:\workspace\cli\artifacts\win10-x64\tools\Archiver.exe "-a" "E:\workspace\cli\arti
facts\win10-x64\intermediate\nuGetPackagesArchive.lzma" "E:\workspace\cli\artifacts\win10-x64\intermediate\nuGetPackages
ArchiveFolder"
Adding E:\workspace\cli\artifacts\win10-x64\intermediate\nuGetPackagesArchiveFolder 100% 2112 ms
Archiving files 100% 1099 ms
Compressing 100% 296120 ms
今回の場合、実に全体ビルド時間の7割をここで食っています。ビルド結果ディレクトリを確認したところ、12MB程度の.lzmaファイルがいくつか存在します。
当然、これLZMAの-9圧縮でエグい時間かけてるのでは…という疑いがかかります。

tip: LZMA圧縮部分の時間を削る

src/Microsoft.DotNet.Archiveディレクトリ内にLZMAエンコーダも含めているようですね。呼び出し側はtools/Archiverで、そのproject.jsonを読むと実体はsrc/dotnet-archive/以下であることがわかります。
ここのコマンドラインオプションに圧縮レベル指定があれば平和に解決したのですが、https://github.com/dotnet/cli/blob/rel/1.0.0/src/dotnet-archive/Program.cs#L29-L32を読むと残念ながらそのような気の利いたオプションはありませんでした。
諦めて、もう少しまじめにコードを読みます。https://github.com/dotnet/cli/blob/rel/1.0.0/build_projects/dotnet-cli-build/CompileTargets.cs#L311-L337のあたりがアーカイブの圧縮をおこなっている部分です。
とてもまじめな改善方針としては「ここで使っているLZMA圧縮のC#実装を改善して、圧縮レベルを指定できるようにする」というものが立ちます。しかしあいにく私はLZMAについて「圧縮がめっちゃ遅いけど展開が速くて圧縮率も良い、雰囲気bzip2をさらにエッジーにしたようなやつ」という程度しか知りません。アルゴリズムに改修を加えるのは荷が重いです。
Cmdの呼び出し部分をしれっと7-zipへ変更して-1をくっつける、というお手頃策も考えはしましたが、ここで圧縮アルゴリズムがLZMAであることは分かってもファイル構造が7-zip準拠かどうかは分かっていないので避けました。
LZMA圧縮/展開部分のコードが確実にアーカイブ作成時と展開時に共有される前提が立つならば、「LZMAで圧縮したように見せかけてストリームを右から左へコピーする偽圧縮アルゴリズムに差し替える」と方針も考えられます。しかし、展開箇所をきっちり調べずに手を動かし始めるのは危険そうな手段です。
結果、より強引な「このアーカイブがビルド成否確認の上で必要かイマイチわからないし生成スキップする」という策をとりました。これは、lzmaでCliコードをgrepかけたところ、少なくともこのファイルが後のビルドフェーズで参照されることはなさそうと判断したためです。
さて、NuGetアーカイブを作成するか否かを決めているCompileCliSdkメソッドは幸いCompileTargets.cs内からしか呼ばれていません。このため変更すべきはhttps://github.com/dotnet/cli/blob/rel/1.0.0/build_projects/dotnet-cli-build/CompileTargets.cs#L113のみです。容赦なくgenerateNugetPackagesArchive: true記述をgenerateNugetPackagesArchive: falseへ変更しましょう。
もちろん、src/Microsoft.DotNet.Cli.Utilsを辿っていくとnuGetPackagesArchiveを展開するくだりが存在するので、このhackは到底production向けのものではありません。「とにかく複数パッケージ間での依存関係に問題がなく、ビルドを通せるのかどうか」を早く知りたい場合には不要というだけです。実際にビルドしたパッケージ群を利用して実行テストをおこなう際には、LZMA圧縮のスキップはおこなわないほうが良いでしょう。

全部あわせてビルド時間が1/20になった

LZMAのアーカイブ生成をスキップする対応も含めて再度ビルドしたところ、私のMacBook上では3分ちょうど(2分59秒)、Windows PC上では1分半(1分30秒)でビルドが完了するようになりました。この程度なら待てます。MacBook上でのビルド時間を94.7%削れたので、20倍高速化したといってよさそうですね。
コードをサクサクと変更して最低限の整合性チェックまでをおこなうことができて、今度こそめでたしめでたし、です。

0 件のコメント:

コメントを投稿