突然ですが、皆さんsbtのchrome trace吐き出す機能使ってますか?大抵の人は使ってないというか、存在すら知らないと思います。
https://github.com/sbt/sbt/pull/4576
以前以下のあたりでも多少話を出しました
https://xuwei-k.hatenablog.com/entry/2022/03/30/142529
さて、それを使ってsbtのbuildのsub project毎のcompile速度を眺めて、頭の中でDAGを描いてみたときに、稀に、
「testのためのユーティリティを参照させるために test->test
を追加しているが、そのせいで遅い」
というパターンがあると思います。
ここに共感してもらわないと話が始まらないのですが、そこを詳しく説明すると長くなるので、多少、大雑把な説明で済ませますが、
build.sbtが以下の構成
val common = Def.settings( scalaVersion := "2.13.10", libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.17.0" % Test ) val a = project .settings( common ) val b = project .settings( common ) .dependsOn( a % "compile->compile;test->test" )
ファイル一覧は以下(すごく単純化してあるので、本当はもっと多いが)
- a/src/main/scala/A.scala
- sub project
a
におけるmainのファイル
- sub project
- a/src/test/scala/ASpec.scala
A
に対する普通のテスト。他のsub projectからは参照されない
- a/src/test/scala/TestInstances.scala
A
のscalacheckのArbitraryのinstanceなどが入る。sub projectのb
などからも参照される
- b/src/main/scala/B.scala
- sub project
b
におけるmainのファイル。A
を参照するかもしれない
- sub project
- b/src/test/scala/BSpec.scala
B
のテスト。sub projecta
のTestInstances
も参照する
こういうパターンの場合 dependsOn で test->test
があるのは自然なのですが、よく見ると、上記のパターンでは細かい無駄がありますね?
無駄とは
「ASpec.scalaのコンパイルは待たずに、sub project b
の Test/compile
は開始可能」
(TestInstances.scalaのコンパイルは待つ必要はある)
です。
こんな無駄が有意なボトルネックになることはあまり多くないのですが、とはいえ数十万行もあるprojectだと、たまにそういうことが発生します。
また、例として scalacheckのArbitraryのinstance
と言いましたが、別に複数のsub project間で参照したいもので、しかしmain側に置きたくないものならば、なんでも良いです。
例えば、テスト用の共通の親classやtraitを作ることはよくあるでしょう。
さて、これの解決策としては、例えば以下のようなものもあります
- main側にscalacheckの依存を追加してしまって、コンパニオンにArbitraryのinstance定義してしまう
- 定義場所が一意に定まってわかりやすい?
- main側に追加するデメリット
- sbtに詳しくなる必要はないメリット
- ボトルネックになるのならば、専用のsub projectを追加で作る
- 手動で作るのは面倒
- 必要なところだけ作るならば、どこがボトルネックなのか?を考える必要がある
- あるいは一括で全部作る(sub project数が倍増する)、というデメリット
それら以外の解決策として、タイトルの
sbtの独自Configuration
を、ここから解説していきます
Configuration
とは、sbtにそういう名前のclassが存在します
- https://github.com/sbt/librarymanagement/blob/90795c3590f54cbd566b4dff74894145a5dd1b3e/core/src/main/scala/sbt/librarymanagement/Configuration.scala
- https://github.com/sbt/sbt/blob/4e7fefa70cda0cac2683991c33abbc9da9c62bf9/sbt-app/src/main/scala/sbt/Import.scala#L262-L263
Configurationの具体的なinstanceとしては
- Compile
- Test
- Runtime
- Provided
などです。
さて、これは独自に作って拡張することが可能です。以下が実際のコード例です。
+val TestShared = Configuration.of("TestShared", "test-shared") extend Compile + val common = Def.settings( scalaVersion := "2.13.10", - libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.17.0" % Test + libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.17.0" % "test,test-shared", + Test / internalDependencyClasspath ++= (TestShared / fullClasspath).value, + TestShared / internalDependencyClasspath ++= (Compile / fullClasspath).value, + inConfig(TestShared)(Defaults.compileSettings) ) val a = project + .configs(TestShared) .settings( common ) val b = project + .configs(TestShared) .settings( common ) .dependsOn( - a % "compile->compile;test->test" + a % "compile->compile;test-shared->test-shared" )
- 独自Configugationを作る
- 今回はTestSharedとしたが、名前はある程度任意に設定可能
extend Compile
部分が最善なのか?が謎
- それをsub projectに
configs
で追加する - libraryDependenciesで
% Test
としてるものも、必要に応じて変更 - test間で共通で参照したいファイルは
src/test/scala
からsrc/test-shared/scala
へ移動する- 共通で参照しないものは
src/test/scala
に残す
- 共通で参照しないものは
- Compile -> TestShared -> Test となるようにclasspathなど設定する
- ここの設定が最善なのか?が怪しい
- sub-project間の依存は、基本的に
test->test
は全てやめて、代わりにtest-shared->test-shared
を追加する
こうすることにより
「ASpec.scalaのコンパイルは待たずに、sub project b
の Test/compile
は開始可能」
という問題点が解消されました。
実例だと、この仕組みを入れた場合、某社のcompile時間が、おそらく最大1〜2割程度縮む可能性はあります。 (まだ入れるかどうか未決定)
1〜2割程度縮む代わりに「sbtの独自Configuration」というあまり見慣れない複雑な仕組みを入れることになるので、このトレードオフをどう捉えるか?によって導入するべきか考えましょう。
例えば、scalafixAllやscalafmtAllなどが test-shared
についても同様に動くするようにするために、追加の設定が必要になるなど、結構なsbtの知識が要求されることになります。
ちなみに独自Configurationといえば、以前こういう記事も書きました