mike-neckのブログ

Java or Groovy or Swift or Golang

今更だけど、Gradleをちゃんと勉強しよう - Gradleのタスク実行フェーズ その2

f:id:mike_neck:20150818084108g:plain

Gradleでなんらかのタスクを作って、実行してみると遅い時がありますね。

まあ、大量のファイルをコピーしてzipで固めるなんて場合は、遅いのは仕方ないです。しかし、cleanとかtasksとかコピーと関係のないタスクまで遅くなるようなことがあれば、設定フェーズでなんらかの重たい処理が必要もないのに走っていることがだいたい原因だったりします。


例えば、次のようなタスクを作ることにします。

  1. payara-microのjarをコピーしてzipにしてbuild/payaraディレクトリーに保存する
  2. jarの中のファイルのリストをテキストファイルに書き出す
  3. 結構大量のファイルをコピーするタスクなので、2.の結果のテキストファイルを前回の結果と比較して、同じものであればUP-TO-DATEとする

関係ないタスクまで遅くなるビルドスクリプト

何も考えずに上記のタスクを書くと、このようになります。

apply plugin: 'base'
ext {
  payaraVersion = '4.1.153'
}
repositories {
  mavenCentral()
}
configuration {
  payara
}
dependencies {
  payara "fish.payara.extras:payara-micro:${payaraVersion}"
}
task copyPayara(type: Zip) {
  // zipファイルの保存先ディレクトリー
  destinationDir = file("${buildDir}/payara")
  // zipファイルの中身のリスト
  def listFile = file("${buildDir}/payara/list.txt")

  // コピーするファイルの定義
  def list = configurations.payara.findAll{
      !it.directory
  }.collect{
      zipTree(it)
  }
  def contents = list.files.collect{it.absolutePath}.join('\n')

  // zipタスクの実行内容
  from list
  doLast {
    listFile.write(contents, 'UTF-8')
  }

  // UP-TO-DATEの設定
  outputs.upToDateWhen {
    listFile.exists() && listFile.text == contents
  }
}

これがどれだけ遅いか試してみましょう。cleanタスクを3回連続で実行します。

$ gradle --daemon clean
:clean

BUILD SUCCESSFUL

Total time: 18.971 secs
$ gradle --daemon clean
:clean

BUILD SUCCESSFUL

Total time: 18.757 secs
$ gradle --daemon clean
:clean

BUILD SUCCESSFUL

Total time: 20.131 secs

たかだかディレクトリーを消すだけのタスクに18秒もかかるのは遅すぎますね。

さらに注目して欲しいのが、cleanタスクが毎回UP-TO-DATEではないという点です。

これは、プロジェクトを評価する際に必ずbuildディレクトリーが生成されているということが原因です。

具体的にはスクリプトの次の部分が問題です。

  // コピーするファイルの定義
  def list = configurations.payara.findAll{
      !it.directory
  }.collect{
      zipTree(it)
  }
  def contents = list.files.collect{it.absolutePath}.join('\n')

設定フェーズでファイルの一覧を構築する際に一度jarをアーカイブから通常のツリーに戻しているために、時間がかかります。

かといって、タスクこのcopyPayaraというタスクを実行するたびに同じことをしてしまうのも時間がかかってしまうのは嫌なので、UP-TO-DATEの設定は残したい…

できるだけ必要のないときには、この部分は動かしたくないという時に用いるのがTaskExecutionGraphです。

TaskExecutionGraphを用いた速いビルドスクリプト

TaskExecutionGraphは実際に実行されるタスクの管理を行うクラスで、このインスタンスに問い合わせることでこれから実行されるタスクが何であるかを取得することができます。。TaskExecutionGraphgradleプロパティのtaskGraphプロパティから取得できます。そして、TaskExecutionGraphwhenReady(Closure)にて時間のかかる処理を記述します。whenReadyはプロジェクトの初期化・設定フェーズが完了して、実行フェーズに移行する直前に起動されます。whenReadyにはTaskExecutionGraph自体が引数として渡されます。

// copyPayaraは保存先の設定だけにする
task copyPayara(type: Zip) {
  destinationDir = file("${buildDir}/payara")
}
gradle.taskGraph.whenReady {graph ->
  def cpTask = tasks.copyPayara
  // タスクグラフがcopyPayaraタスクを行う場合のみ追加の設定を行う
  if(graph.hasTask(cpTask)) {
    def listFile = file("${buildDir}/payara/list.txt")
    def list = configurations.payara.findAll{
        !it.directory
    }.collect{
        zipTree(it)
    }
    def contents = list.files.collect{it.absolutePath}.join('\n')
    cpTask.from list
    cpTask.doLast {
      listFile.write(contents, 'UTF-8')
    }
    cpTask.outputs.upToDateWhen {
      listFile.exists() && listFile.text == contents
    }
  }
}

では、このビルドスクリプトで、cleanタスクを3回行ってみます。

$ gradle --daemon clean
:clean UP-TO-DATE

BUILD SUCCESSFUL

Total time: 1.09 secs
$ gradle --daemon clean
:clean UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.758 secs
$ gradle --daemon clean
:clean UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.75 secs

この通り、cleanタスクの実行時間が短くなりましたし、clean自体も常にUP-TO-DATEになりました。


もしタスク開始までの時間が遅いビルドがあって、かつcleantasksといった普通のタスクまでもが時間がかかっているようでしたら、ビルドスクリプトを見なおしてみるとよいかもしれません。

amazon.co.jp