Julia言語開発におけるCI
門脇 宗平
こんにちは。ATL客員研究員の門脇です。普段はMITのJuliaLabという組織のResearch Programmerとしてプログラミング言語Juliaのコンパイラの開発に携わっています。
このポストでは、Julia言語の開発に用いられているCIパイプラインと、それらのCIの恩恵を強く感じたエピソードについてご紹介したいと思います。
Julia言語のCIインフラ
CIはソフトウェアの開発効率を高く保つ上で欠かせない存在ですが、その目的や手法は各プロジェクト様々だと思います。 Julia言語の開発レポジトリ JuliaLang/julia
でも非常に多くのCIがセットアップされており、僕が認識しているものだけでも以下のパイプラインが存在します:
1
: コミットごとに走るCIパイプライン1.1
: ビルドテスト1.2
: ユニットテスト1.3
: その他の細かいフォーマットチェックなど
2
: 日ごとに走るCIパイプライン2.1
: パッケージリグレッションテスト2.2
: パフォーマンスリグレッションテスト
1
のCIパイプラインは、一般的なOSSと同じく、 GitHub Actions や Buildbot、Buildkite といったCI/CDサービスを利用して行われている、比較的小規模なCIパイプラインです。
一方で、 2
のCIパイプラインは普段あまり見かけないような非常に大規模なパイプラインです。
まず 2.1
は、Juliaの公式パッケージレジストリに登録されている全ての パッケージのテストスイートを実行し結果を比較するパイプラインで、 "PkgEval CI" と呼ばれています 1。現在公式レジストリには 6593 ものパッケージが登録されており 2、 2021年8月に取られたサーベイではそのうちおよそ 90% が何かしらのテストコードを含んでいた 3 そうなので、単純計算で 2.1
のパイプラインを実行するためにはおよそ 6000 ものパッケージをダウンロードし、そのテストスイートを実行する必要があります.
ついで 2.2
のパフォーマンスリグレッションテストは、 BaseBenchmarks.jl
と呼ばれるベンチマークスイートを利用して、実行時間やメモリアロケーションなどを比較するパイプラインです。 BaseBenchmarks.jl
は JuliaLang/julia
レポジトリ内の Base
モジュールや標準ライブラリを対象とした巨大なベンチマークスイートで、現在のところ 4126 個ものベンチマークセットが含まれています 4。それぞれのベンチマークセットは10秒程度で終わるものから10分程度かかってしまうものまで様々ですが、このベンチマークスイートを実行するだけでもかなりのリソースが必要なことがわかるかと思います。
このように、これら2
のCIパイプラインを実行するためには一般的な計算リソースでは時間がかかりすぎてしまうため、実行に際してはMITが管理する巨大な計算クラスタ上へと投入され実行されます。ただそれでも、それぞれ10時間程度はかかるような巨大なパイプラインであるため、これらのパイプラインはコミット/PRごとには起動されず、日毎あるいはGitHub上でのコメントを経由して不定期に起動され、集計された結果やログは専用のGitHubレポジトリへpushされ管理、場面に応じて呼び出し元のPRやコミットにマークダウンとしてコメントされる、といったような運用が行われています5。
bisect × performance CI = 😀
上述のように、非常に大規模なCIが運用されているJulia言語の開発ですが、つい先日この 2.2
のパフォーマンスリグレッションテストの恩恵を強く感じるエピソードがあったので簡単に紹介したいと思います。
現在Juliaの開発チームは次の安定版であるversion 1.7のリリースに向けて動いており、リリース候補版 release candidateに対する利用者たちからのフィードバックを集め、適宜改善修正を行なっています。そんな中、僕がv1.7で加えた型推論/インライン最適化変更分が原因と思われるパフォーマンスリグレッションに関するイシュー (#42754
) が上げられたので、早速改善することにしました。
300行程度の少し込み入ったPR (#42766
) を送り、レビューを受け、パフォーマンスリグレッションテストも問題なさそうな結果だったので、最後ついでにコスメティックな細かい修正を加えてそのままマージし、イシューもクローズでき、また一歩v1.7に近づくことができてめでたしめでたし…
…となるかと思っていたのですが、マージの数日後、メモリアロケーションの増加というまた別のリグレッションがパフォーマンスCIで報告されてしまいました。比較元コミットとその時点での最新のコミットの間には、上記のPRのコミットを含め15コミットほど存在していたのですが、同僚の方が git bisect
の要領で各コミットに対して二分探索的にパフォーマンスリグレッションテストを走らせ、とても効率的に僕のPRが直接の原因であることを突き止めてくれました6。
結果としては、僕が先ほどのPRをマージする直前に加えた「コスメティックな細かい修正」がインライン最適化の細かい挙動を変更してしまっていたことが原因でした。パフォーマンスリグレッションの原因を速やかに特定できたおかげで、すぐに追加修正を加えることができ(わずか1行の変更でした)、無事v1.7にこの恥ずかしいミスを入れ込まずに済んだのですが、たった1行のミスでパフォーマンスがガラッと変わってしまうソフトウェア開発は面白いなと思うとともに、こうした細かいパフォーマンスのリグレッションを発生からわずか数日で検出できるパフォーマンスCIのありがたみを改めて実感する出来事でした。
まとめ
このポストでは、Julia言語のCIインフラの話から、パフォーマンスCIについてご紹介しました。今回感じたことを簡単にまとめて締めさせていただきます:
- インテグレーションやパフォーマンスに関する、リソースが必要であったり、確度の高いテストが難しかったりするものでも、開発者が簡単に実行可能なCI環境があると非常に便利
- 特に
git bisect
のスクリプトとして渡せる/あるいは同等の二分探索が行える程度に自動化された状態だと、効率的にリグレッションの原因が特定可能
リンク
- GitHub: https://github.com/aviatesk
- Twitter: https://twitter.com/kdwkshh
- Blog (English): https://aviatesk.github.io/
-
Juliaパッケージのテストスイートは
test/runtests.jl
というファイルをトップレベルスクリプトとして起動される決まりになっているので、こうした統一的なテスト実行が容易です。↩︎ - 2021/11/01に https://juliahub.com/ui/Packages へアクセスして確認。↩︎
- サーベイはhttps://julialang.org/blog/2021/08/general-survey/を参照。上記の「何かしらのテストコードを含」むパッケージは、パッケージテンプレートが生成する自動生成テストコードの行数よりも多くのテストコードを含むパッケージをカウント。↩︎
-
以下のコードで確認:
123456789using BenchmarkTools, BaseBenchmarkscount_benchmark(g::BenchmarkTools.BenchmarkGroup) =# "parallel" suite may not be loadedisempty(g.data) ? 0 : sum(((k,v),)->count_benchmark(v), g.data)count_benchmark(::BenchmarkTools.Benchmark) = 1suite = BaseBenchmarks.loadall!()count_benchmark(suite) # => 4126
↩︎ -
この
2
のCIパイプラインは https://github.com/JuliaCI/Nanosoldier.jl で管理されています。↩︎ -
2c03f811da9962738dc5b7f8ecc17ae00988f94e
や404e584165fdd83df977688f288ca4d35e0c85d1
を見ると、 JuliaコンパイラチームのJameson Nashさんが僕のコミットを追い詰めていく様が確認できます。↩︎