OSSにしてpublishしようかと思っていたけど、細かい部分をいい感じにするのが面倒なので一旦雑に貼り付けておく
(publishは後でやるかもしれない、未定)
- https://github.com/xuwei-k/scalaz/commit/06488c412b2fac78fe33739ae4f7abdfb5c49164
- https://github.com/xuwei-k/scalaz/actions/runs/9326417427
package sbt // package privateなclassを参照したいので import java.io.ByteArrayOutputStream import java.nio.charset.StandardCharsets import sbt.internal.AbstractTaskExecuteProgress import sbt.internal.ShutdownHooks import scala.collection.compat.* import scala.concurrent.duration.* /** * sbt標準のchrome trace形式ではなく、GitHubのmarkdownでそのまま表示できる形式で出力する用 * [[https://github.com/sbt/sbt/blob/eec3c32cc841365799eaa5460272e8287b975f10/main/src/main/scala/sbt/internal/TaskTraceEvent.scala]] * [[https://github.com/sbt/sbt/pull/4576]] * [[https://mermaid.js.org/syntax/gantt.html]] */ class MermaidGanttChart extends AbstractTaskExecuteProgress with ExecuteProgress[Task] { import AbstractTaskExecuteProgress.Timer override def initial(): Unit = () override def afterReady(task: Task[?]): Unit = () override def afterCompleted[T](task: Task[T], result: Result[T]): Unit = () override def afterAllCompleted(results: RMap[Task, Result]): Unit = () override def stop(): Unit = () ShutdownHooks.add(() => report()) private[this] def report() = { if (anyTimings) { writeAll() } } def taskFilter(name: String, durationMicros: Long): Boolean = { // 一定以上時間がかかったものだけを記録 durationMicros > 3.seconds.toMicros } /** * 時間がかかった順でtask名と秒数を出力 */ private def writeSlowTaskList(values: List[(Task[?], Timer)], write: String => Unit): Unit = { if (values.nonEmpty) { write("") write("## 時間がかかっているtaskの一覧") write("") values .takeRight(20) .reverseIterator .foreach { case (task, value) => write(s"""1. ${simplifyTaskName(task)} """ + "%.1f".format(value.durationMicros / 1000000.0)) } write("") } } /** * 表示上の見やすさのために、区別がつく範囲で名前を短くする */ private def simplifyTaskName[A](t: Task[A]): String = { taskName(t) .replace(" ", "") .replace("/executeTests", "/test") .replace("compileIncremental", "compile") } private def writeAll(): Unit = { val out = new ByteArrayOutputStream() val values = currentTimings .filter(x => taskFilter(taskName(x._1), x._2.durationMicros)) .toList .sortBy(_._2.durationMicros) .takeRight(100) // 多く出力しすぎても見づらいので、時間がかかった順で固定数にする val write: String => Unit = { s => out.write(s.getBytes(StandardCharsets.UTF_8)) out.write('\n') } writeTraceEvent(values, write) writeSlowTaskList(values, write) val result = out.toByteArray sys.env.get("GITHUB_STEP_SUMMARY").map(file).filter(_.isFile).foreach { summary => // 存在している場合は、GitHub Actionsが用意してるファイルにも直接書きこむ IO.append(summary, result) } // CIのことだけを考えれば上記だけで十分であるが、ローカルの動作確認用に別のファイルにも書き込む IO.write(file("target") / "traces" / "trace.md", result) } private def writeTraceEvent(values: List[(Task[?], Timer)], write: String => Unit): Unit = { values.map(_._2.startMicros).minOption match { case Some(start) => write("") write("```mermaid") write("gantt") write(" dateFormat x") write(" axisFormat %M:%S") val grouped = values.groupBy(_._2.threadId) val nowMillis = System.currentTimeMillis() def normalize(n: Long): Long = (n - start) + nowMillis def durationEvent(name: String, t: Timer): String = { val start = normalize(t.startMicros / 1000) val end = normalize((t.startMicros + t.durationMicros) / 1000) s""" ${name} : ${start} , ${end}""" } grouped.foreach { case (threadId, values) => write(s" section ${threadId}") values.sortBy(_._2.startMicros).foreach { case (key, value) => val name = simplifyTaskName(key) val n = name + " " + "%.1f".format(value.durationMicros / 1000000.0) write(" " + durationEvent(n, value)) } } write("```\n\n") case None => println("時間がかかったtaskが存在しませんでした") } } }
project/適当な名前.scala
に上記のファイルを置いて、あとはトップレベルの なんとか.sbt
に以下を書いておけば、あとは何もしなくてもgithub actionsで実行するだけで全自動で出ます。
progressReports += Keys.TaskProgress(new MermaidGanttChart())
GitHubのこの仕様自体の詳細な解説はしないので、公式ドキュメント読んでください。
GITHUB_STEP_SUMMARY
という特別に用意された環境変数を解決した文字列をファイル名として扱って、そこに追記していけば、shell scriptからではなくても直接Scalaなど任意の言語から書き込めます。
このchartの記法のドキュメントはこれ?
https://mermaid.js.org/syntax/gantt.html
そもそもGantt Chartをsbtのこのtaskの表示に使うのは適切なのか???というと、おそらく微妙ですが、他にいい感じのものがなかったので、これで無理やり代用しました。 threadごとに分けないと(重ねてしまうと)表示されない仕様らしいので、あえてthreadごとに分けています。
元々のchromeの方式で見た方が、拡大縮小や、その他クリックすると詳細情報が表示されて圧倒的に見やすいですが、これの方が何もしなくても気軽に見れて、ある程度継続的に自動で保存されるメリットがあります。
OSSだと基本的に無料なので、細かい効率気にせずに分割して富豪的にCIのリソース使って実行してることが大半なので、分割した状態で実行しても、あまりインパクトがないというか、嬉しさがわからないかもしれません。
今回のサンプルも、あえて普通とは違って全部まとめてcompileにしてみました。
jvmか?jsか?nativeか?にかかわらず、明らかにcore部分のcompileに時間がかかっていることがわかりますね。 これはcompileに限らず、testでもなんでも、任意のsbtのtaskで出力が可能です。
若干余談になりますが、mermaidの仕様的には以下のようなtextを吐き出しているだけなので、sbtのtask関係なく、割と気軽にchart吐き出して利用するのは便利なので、いろいろなものを出してみると面白いと思います。
gantt chart以外にも種類がいっぱいあるので。
gantt dateFormat x axisFormat %M:%S section 247 effectJVM/Compile/compile 5.1 : 1716968012765 , 1716968017869 section 224 coreJS/Compile/compile 148.6 : 1716967869059 , 1716968017678 iterateeNative/Compile/compile 7.5 : 1716968022677 , 1716968030148 scalacheckBindingJS/Compile/compile 10.8 : 1716968032347 , 1716968043104 testsJVM/Test/compile 59.4 : 1716968043325 , 1716968102712 section 225 coreJVM/Compile/compile 143.4 : 1716967867846 , 1716968011243 effectNative/Compile/compile 6.3 : 1716968016365 , 1716968022633 exampleJVM/Compile/compile 11.6 : 1716968023106 , 1716968034742 testsJS/Test/compile 55.6 : 1716968043437 , 1716968098999 section 245 effectJS/Compile/compile 6.2 : 1716968018448 , 1716968024683 scalacheckBindingNative/Compile/compile 11.0 : 1716968032100 , 1716968043146 section 241 coreNative/Compile/compile 147.4 : 1716967867810 , 1716968015199 iterateeJS/Compile/compile 7.3 : 1716968024796 , 1716968032100 exampleJS/Compile/compile 13.0 : 1716968034742 , 1716968047768 section 250 scalacheckBindingJVM/Compile/compile 9.1 : 1716968023101 , 1716968032234 testsNative/Test/compile 56.0 : 1716968045070 , 1716968101088 section 246 iterateeJVM/Compile/compile 5.1 : 1716968017947 , 1716968023045 exampleNative/Compile/compile 14.6 : 1716968030191 , 1716968044817