Secrets of the Build Scan Plugin - Virtual Android Makers 2020

Secrets of the Build Scan Plugin - Virtual Android Makers 2020

Build scans provide some very useful information about Gradle Builds. Build time, configuration time, task execution time. Scans even show garbage collection time, time spent downloading dependencies, time spent in annotation processors? What can you do with all this data?

In this talk I'll share how to surface actionable information from your Gradle build immediately to developers without using the build scan plugin or Gradle Enterprise.

Presented at Virtual Android Makers https://androidmakers.fr/schedule/2020-04-20?sessionId=172401
https://www.youtube.com/watch?v=lgaqS0pmUzk

5f69045a2ca496221cfc624405917cdf?s=128

Nelson Osacky

April 20, 2020
Tweet

Transcript

  1. Secrets of the Gradle Build Scan Plugin Nelson Osacky

  2. Secrets of the Gradle Build Scan Plugin Nelson Osacky

  3. Secrets of the Gradle Enterprise Gradle Plugin Nelson Osacky

  4. None
  5. Me • Previously at Square in SF on Square Register

    and Build Tools • Currently Working at SoundCloud in Berlin
  6. Me • Je parle francais ! • J'ai étudié à

    l’Université de Technologie de Compiègne
  7. None
  8. Me • Maintainer of some Gradle Plugins: • Fladle -

    Plugin for Firebase Test Lab https://github.com/runningcode/ fladle • Delect - Plugin for Dagger Reflect https://github.com/soundcloud/ delect • Gradle Doctor - https://github.com/runningcode/gradle-doctor
  9. None
  10. What is a build scan?

  11. BUILD SUCCESSFUL in 14s 1 actionable task: 1 executed Publishing

    build scan... http://gradle-enterprise.dev.s-cloud.net:81/s/4ctreiklpx5da
  12. None
  13. What are we solving?

  14. Mismatched JDK causes extra Gradle Daemon

  15. Excessive GC time causes slow builds

  16. Annotation processing time can be skipped with Dagger Reflect

  17. Annotation processing time can be skipped with Dagger Reflect

  18. Nobody checks build scans

  19. Too much information

  20. Build Scan Data -> Actionable Insights

  21. Mismatched JDK

  22. Mismatched JDK causes extra Gradle Daemon

  23. Jetbrains s.r.o JDK Oracle Java SE

  24. Mismatched JDK causes extra Gradle Daemon

  25. Jetbrains s.r.o JDK Oracle Java SE

  26. 6GB 6GB 4GB

  27. 2 x 6GB Daemon + 4GB AS = 16GB We

    all have 16GB MacBooks What about other apps? Web browser?
  28. How can we fix it?

  29. 6GB 6GB 4GB

  30. Don’t use built-in JDK, use JAVA_HOME

  31. None
  32. None
  33. Add instructions for the fix to Onboarding

  34. Nobody follows instructions

  35. None
  36. Nobody reads warnings

  37. Send everyone an email?

  38. Make a presentation?

  39. Walk around to teammates’ desks?

  40. Let’s solve this programmatically

  41. Disallow multiple Daemons by failing the build

  42. 1st attempt Detect multiple daemons

  43. $ ps aux

  44. None
  45. $ ps aux | grep GradleDaemon

  46. no 2638 0,0 0,8 12502224 128092 ?? Ss 11:21am 3:25.62

    /Library/Java/ JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java -Xmx6g -Dfile.encoding=UTF-8 - Duser.country=DE -Duser.language=de -Duser.variant -cp /Users/no/.gradle/wrapper/dists/gradle-6.1.1-all/ cfmwm155h49vnt3hynmlrsdst/gradle-6.1.1/lib/gradle-launcher-6.1.1.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.1.1 no 1485 0,0 15,3 12706900 2574060 ?? Ss 11:10am 13:34.74 /Users/no/Library/ Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/192.6186006/Android Studio.app/Contents/ jre/jdk/Contents/Home/bin/java -Xmx6g -Dfile.encoding=UTF-8 -Duser.country=DE -Duser.language=de - Duser.variant -cp /Users/no/.gradle/wrapper/dists/gradle-6.1.1-all/cfmwm155h49vnt3hynmlrsdst/ gradle-6.1.1/lib/gradle-launcher-6.1.1.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.1.1 no 1315 0,0 0,7 10615736 122396 ?? Ss 11:07am 1:39.22 /Library/Java/ JavaVirtualMachines/jdk-12.0.1.jdk/Contents/Home/bin/java --add-opens java.base/java.util=ALL-UNNAMED -- add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED -Dfile.encoding=UTF-8 -Duser.country=DE -Duser.language=de - Duser.variant -cp /Users/no/.gradle/wrapper/dists/gradle-6.1.1-all/cfmwm155h49vnt3hynmlrsdst/ gradle-6.1.1/lib/gradle-launcher-6.1.1.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.1.1 no 1207 0,0 0,7 10274128 125368 ?? Ss 11:06am 1:55.71 /Library/Java/ JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java -Dfile.encoding=UTF-8 -Duser.country=DE - Duser.language=de -Duser.variant -cp /Users/no/.gradle/wrapper/dists/gradle-6.0.1-all/ 99d3u8wxs16ndehh90lbbir67/gradle-6.0.1/lib/gradle-launcher-6.0.1.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.0.1 no 6366 0,0 0,0 4267972 680 s003 R+ 1:20pm 0:00.00 grep GradleDaemon
  47. no 2638 0,0 0,8 12502224 128092 ?? Ss 11:21am 3:25.62

    /Library/Java/ JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java -Xmx6g -Dfile.encoding=UTF-8 - Duser.country=DE -Duser.language=de -Duser.variant -cp /Users/no/.gradle/wrapper/dists/gradle-6.1.1-all/ cfmwm155h49vnt3hynmlrsdst/gradle-6.1.1/lib/gradle-launcher-6.1.1.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.1.1 no 1485 0,0 15,3 12706900 2574060 ?? Ss 11:10am 13:34.74 /Users/no/Library/ Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/192.6186006/Android Studio.app/Contents/ jre/jdk/Contents/Home/bin/java -Xmx6g -Dfile.encoding=UTF-8 -Duser.country=DE -Duser.language=de - Duser.variant -cp /Users/no/.gradle/wrapper/dists/gradle-6.1.1-all/cfmwm155h49vnt3hynmlrsdst/ gradle-6.1.1/lib/gradle-launcher-6.1.1.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.1.1 no 1315 0,0 0,7 10615736 122396 ?? Ss 11:07am 1:39.22 /Library/Java/ JavaVirtualMachines/jdk-12.0.1.jdk/Contents/Home/bin/java --add-opens java.base/java.util=ALL-UNNAMED -- add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.prefs/java.util.prefs=ALL-UNNAMED -Dfile.encoding=UTF-8 -Duser.country=DE -Duser.language=de - Duser.variant -cp /Users/no/.gradle/wrapper/dists/gradle-6.1.1-all/cfmwm155h49vnt3hynmlrsdst/ gradle-6.1.1/lib/gradle-launcher-6.1.1.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.1.1 no 1207 0,0 0,7 10274128 125368 ?? Ss 11:06am 1:55.71 /Library/Java/ JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java -Dfile.encoding=UTF-8 -Duser.country=DE - Duser.language=de -Duser.variant -cp /Users/no/.gradle/wrapper/dists/gradle-6.0.1-all/ 99d3u8wxs16ndehh90lbbir67/gradle-6.0.1/lib/gradle-launcher-6.0.1.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.0.1 no 6366 0,0 0,0 4267972 680 s003 R+ 1:20pm 0:00.00 grep GradleDaemon
  48. $ ps aux | grep GradleDaemon | wc -l

  49. $ ps aux | grep GradleDaemon | wc -l 5

  50. Gradle Time!

  51. Gradle Time! • Samples can be placed in any build.gradle.kts

    file • Can also be placed in build.gradle with minor modifications • Can also be placed in buildSrc • In Gradle Doctor it is packaged inside the plugin
  52. • Run command ps command • Check if more than

    one daemon • Display warning
  53. $ ps aux | grep GradleDaemon | wc -l

  54. valacommanda=aarrayOf("/bin/bash", "-c", "ps aux | grep GradleDaemon | wc -l")

    build.gradle.kts
  55. valacommanda=aarrayOf("/bin/bash", "-c", "ps aux | grep GradleDaemon | wc -l")

    valbprocessb=bRuntime.getRuntime().exec(command) build.gradle.kts
  56. valacommanda=aarrayOf("/bin/bash", "-c", "ps aux | grep GradleDaemon | wc -l")

    valbprocessb=bRuntime.getRuntime().exec(command) if (process.waitFor() != 0) {a throw RuntimeException(process.errorStream.readToString()) }a build.gradle.kts
  57. valacommanda=aarrayOf("/bin/bash", "-c", "ps aux | grep GradleDaemon | wc -l")

    valbprocessb=bRuntime.getRuntime().exec(command) if (process.waitFor() != 0) {a throw RuntimeException(process.errorStream.readToString()) }a process.inputStream.readBytes().toString().trim().toInt() - 1 build.gradle.kts
  58. valacommanda=aarrayOf("/bin/bash", "-c", "ps aux | grep GradleDaemon | wc -l")

    valbprocessb=bRuntime.getRuntime().exec(command) if (process.waitFor() != 0) {a throw RuntimeException(process.errorStream.readToString()) }a valcnumDaemons = process.inputStream.readBytes().toString().trim().toInt() - 1 build.gradle.kts
  59. valacommanda=aarrayOf("/bin/bash", "-c", "ps aux | grep GradleDaemon | wc -l")

    valbprocessb=bRuntime.getRuntime().exec(command) if (process.waitFor() != 0) {a throw RuntimeException(process.errorStream.readToString()) }a valcnumDaemons = process.inputStream.readBytes().toString().trim().toInt() - 1 if (numDaemons > 1) {b throw GradleException("$numDaemons active. This may indicate a mismatched JAVA_HOME.") }b build.gradle.kts
  60. First approach

  61. But… • Throws exception when running two different Gradle Projects

    • Throws exceptions for build.gradle.kts spawning extra Gradle Daemons • Throws exceptions when running tests using the GradleRunner
  62. 2nd attempt

  63. We just want JAVA_HOME to match

  64. Use JAVA_HOME Use JAVA_HOME

  65. val environmentJavaHome = System.getenv("JAVA_HOME") Check JAVA_HOME

  66. val gradleJavaHome = Jvm.current().javaHome Check current Gradle’s javaHome

  67. val environmentJavaHomea=bSystem.getenv("JAVA_HOME") Gradle will use JAVA_HOME from terminal, if set

  68. val environmentJavaHomea=bSystem.getenv("JAVA_HOME") val gradleJavaHomec=dJvm.current().javaHome Gradle launched from Android studio will

    use AS embedded JDK by default
  69. val environmentJavaHomea=bSystem.getenv("JAVA_HOME") val gradleJavaHomec=dJvm.current().javaHome ifa(environmentJavaHomea==bnull) {a throwaGradleException("JAVA_HOME isn't set.") }a

  70. val environmentJavaHomea=bSystem.getenv("JAVA_HOME") val gradleJavaHomec=dJvm.current().javaHome ifa(environmentJavaHomea==bnull) {a throwaGradleException("JAVA_HOME isn't set.") }a

    if (gradleJavaHome.path != environmentJavaHome) {b throw GradleException("JAVA_HOME doesn't match Gradle's Java") }b
  71. val environmentJavaHomea=bSystem.getenv("JAVA_HOME") val gradleJavaHomec=dJvm.current().javaHome ifa(environmentJavaHomea==bnull) {a throwaGradleException("JAVA_HOME isn't set.") }a

    if (gradleJavaHome.toPath().toRealPath() != File(environmentJavaHome).toPath().toRealPath()) {b throw GradleException("JAVA_HOME doesn't match Gradle's Java") }b Follow symlinks! Thanks Eugen Martynov!
  72. val environmentJavaHomea=bSystem.getenv("JAVA_HOME") val gradleJavaHomec=dJvm.current().javaHome ifa(environmentJavaHomea==bnull) {a throwaGradleException("JAVA_HOME isn't set.") }a

    if (gradleJavaHome.toPath().toRealPath() != File(environmentJavaHome).toPath().toRealPath()) {b throw GradleException("JAVA_HOME doesn't match Gradle's Java") }b Follow symlinks! Thanks Eugen Martynov!
  73. None
  74. Sharing is caring Sharing JAVA_HOME means sharing Gradle Daemon

  75. None
  76. None
  77. None
  78. Basic build measurements

  79. gradle.addBuildListener(object : BuildListener { override fun settingsEvaluated(settings: Settings) { }

    overrideafunabuildFinished(result:aBuildResult)a{ } override fun projectsLoaded(gradle: Gradle) { } override fun buildStarted(gradle: Gradle) { } override fun projectsEvaluated(gradle: Gradle) { } }) build.gradle.kts
  80. overrideafunabuildFinished(result:aBuildResult)a{ } build.gradle.kts

  81. gradle.buildFinished { } build.gradle.kts

  82. val start = System.currentTimeMillis() gradle.buildFinished { } build.gradle.kts

  83. val start = System.currentTimeMillis() gradle.buildFinished { val finished = System.currentTimeMillis()

    println("Build took ${finished - start}ms") } build.gradle.kts
  84. To see more detail about a task, run gradlew help

    --task <task> For troubleshooting, visit https://help.gradle.org Build took 288ms BUILD SUCCESSFUL in 1s 1 actionable task: 1 executed
  85. None
  86. ManagementFactory.getGarbageCollectorMXBeans()

  87. ManagementFactory. getGarbageCollectorMXBeans()

  88. /** * Returns a list of {@link GarbageCollectorMXBean} objects *

    in the Java virtual machine. * The Java virtual machine may have one or more * {@code GarbageCollectorMXBean} objects. * It may add or remove {@code GarbageCollectorMXBean} * during execution. * * @return a list of {@code GarbageCollectorMXBean} objects. * */ public static List<GarbageCollectorMXBean> getGarbageCollectorMXBeans() { return getPlatformMXBeans(GarbageCollectorMXBean.class); }
  89. ManagementFactory. getGarbageCollectorMXBeans() .sumBy { it.collectionTime.toInt() }

  90. ManagementFactory.getGarbageCollectorMXBeans() .sumBy { it.collectionTime.toInt() }

  91. ManagementFactory.getGarbageCollectorMXBeans() .sumBy { it.collectionTime.toInt() }

  92. valastartGCTimea=aManagementFactory.getGarbageCollectorMXBeans() .sumBya{ait.collectionTime.toInt()a}

  93. valastartGCTimea=aManagementFactory.getGarbageCollectorMXBeans() .sumBya{ait.collectionTime.toInt()a} gradle.buildFinished { val endGCTime = ManagementFactory.getGarbageCollectorMXBeans() .sumBy {

    it.collectionTime.toInt() } val gcTime = endGCTime - startGCTime println("Spent $gcTime ms garbage collecting") }
  94. Spent 17 ms garbage collecting BUILD SUCCESSFUL in 2s

  95. Awesome!

  96. Only print GC time if it is high High GC

    time -> Actionable
  97. valastartGCTimea=aManagementFactory.getGarbageCollectorMXBeans() .sumBya{ait.collectionTime.toInt()a} gradle.buildFinished { val endGCTime = ManagementFactory.getGarbageCollectorMXBeans() .sumBy {

    it.collectionTime.toInt() } val gcTime = endGCTime - startGCTime println("Spent $gcTime ms garbage collecting") }
  98. val start = System.currentTimeMillis() valastartGCTimea=aManagementFactory.getGarbageCollectorMXBeans() .sumBya{ait.collectionTime.toInt()a} gradle.buildFinished { val end

    = System.currentTimeMillis() val endGCTime = ManagementFactory.getGarbageCollectorMXBeans() .sumBy { it.collectionTime.toInt() } val duration = end - start val gcTime = endGCTime - startGCTime println("Build took $duration ms") println("Spent $gcTime ms garbage collecting") println("${gcTime * 1.0f / duration} % garbage collecting") }
  99. Build took 1257 ms Spent 17 ms garbage collecting 0.013524264

    % garbage collecting BUILD SUCCESSFUL in 2s
  100. =============================== Gradle Doctor Prescriptions ============================================ | This build spent 96%

    garbage collecting. | | If this is the first build with this Daemon, it likely means that this build needs more heap space. | | Otherwise, if this is happening after several builds it could indicate a memory leak. | | For a quick fix, restart this Gradle daemon. ./gradlew --stop | ========================================================================================================
  101. Java Annotation Processor events

  102. Annotation processing time can be skipped with Dagger Reflect

  103. None
  104. None
  105. If Gradle Enterprise can do it, so can we!

  106. None
  107. None
  108. None
  109. None
  110. /** * An internal interface for Gradle that exposed objects

    and concepts that * are not intended for public consumption. */ @UsedByScanPlugin public interface GradleInternal extends Gradle, PluginAwareInternal { @UsedByScanPlugin ServiceRegistry getServices(); }
  111. /** * A read-only registry of services. May or may

    not be immutable. */ public interface ServiceRegistry extends ServiceLookup { /** * Locates the service of the given type. * * @param serviceType The service type. * @param <T> The service type. * @return The service instance. Never returns null. * @throws UnknownServiceException When there is no service of the given type available. * @throws ServiceLookupException On failure to lookup the specified service. */ @UsedByScanPlugin <T> T get(Class<T> serviceType) throws UnknownServiceException, ServiceLookupException; }
  112. /** * Manages listeners of build operations. * * There

    is one global listener for the life of the build runtime. * Listeners must be sure to remove themselves if they want to only listen for a single build. * */ public interface BuildOperationListenerManager { void addListener(BuildOperationListener listener); void removeListener(BuildOperationListener listener); BuildOperationListener getBroadcaster(); }
  113. val gradleInternal = (gradle as GradleInternal)

  114. val gradleInternal = (gradle as GradleInternal) val manager = gradleInternal.services[BuildOperationListenerManager::class.java]

  115. val gradleInternal = (gradle as GradleInternal) val manager = gradleInternal.services[BuildOperationListenerManager::class.java]

    manager.addListener(object : BuildOperationListener { override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { } override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { } override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) { } })
  116. manager.addListener(object : BuildOperationListener { override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent)

    { } override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { } override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) { } }) Memory Leak!
  117. Nobody reads warnings

  118. /** * Manages listeners of build operations. * * There

    is one global listener for the life of the build runtime. * Listeners must be sure to remove themselves if they want to only listen for a single build. * */ public interface BuildOperationListenerManager { void addListener(BuildOperationListener listener); void removeListener(BuildOperationListener listener); BuildOperationListener getBroadcaster(); }
  119. There is one global listener for the life of the

    build runtime. Listeners must be sure to remove themselves if they want to only listen for a single build.
  120. There is one global listener for the life of the

    build runtime. Listeners must be sure to remove themselves if they want to only listen for a single build.
  121. manager.addListener(object : BuildOperationListener { override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent)

    { } override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { } override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) { } })
  122. val listener = object : BuildOperationListener { override fun progress(operationIdentifier:

    OperationIdentifier, progressEvent: OperationProgressEvent) { } override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { } override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) { } }
  123. val listener = object : BuildOperationListener { override fun progress(operationIdentifier:

    OperationIdentifier, progressEvent: OperationProgressEvent) { } overrideafunafinished(buildOperation:aBuildOperationDescriptor,afinishEvent: OperationFinishEvent)a{ } override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) { } } manager.addListener(listener) gradle.buildFinished { manager.removeListener(listener) } ☺
  124. overrideafunafinished(buildOperation:aBuildOperationDescriptor,afinishEvent: OperationFinishEvent)a{ }

  125. override fun finished( buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { }

  126. public final class OperationFinishEvent { long startTime; long endTime; Throwable

    failure; Object result; }
  127. None
  128. public final class OperationFinishEvent { long startTime; long endTime; Throwable

    failure; Object result; }
  129. @NotUsedByScanPlugin("used to report annotation processor execution times to TAPI progress

    listeners") public class CompileJavaBuildOperationType implements BuildOperationType { public interface Result { /** * Returns details about the used annotation processors, if available. * * <p>Details are only available if an instrumented compiler was used. * * @return details about used annotation processors; {@code null} if unknown. */ @Nullable List<AnnotationProcessorDetails> getAnnotationProcessorDetails(); /** * Details about an annotation processor used during compilation. */ interface AnnotationProcessorDetails { /** * Returns the fully-qualified class name of this annotation processor. */ String getClassName(); /** * Returns the total execution time of this annotation processor. */ long getExecutionTimeInMillis(); } } }
  130. override fun finished( buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { }

  131. var daggerTime = 0L val listener = object : BuildOperationListener

    { override fun finished( buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { val result = finishEvent.result ifa(result is CompileJavaBuildOperationType.Result)a{ result.annotationProcessorDetails!!.forEacha{ if (it.className.contains("dagger"))a{ daggerTime += it.executionTimeInMillis } } } } }
  132. var daggerTime = 0L val listener = object : BuildOperationListener

    { override fun finished( buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { val result = finishEvent.result ifa(result is CompileJavaBuildOperationType.Result)a{ result.annotationProcessorDetails!!.forEacha{ if (it.className.contains("dagger"))a{ daggerTime += it.executionTimeInMillis } } } } }
  133. result.annotationProcessorDetails!!.forEacha{ if (it.className.contains("dagger"))a{ daggerTime += it.executionTimeInMillis } }

  134. var daggerTime = 0L val listener = object : BuildOperationListener

    { override fun finished( buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { val result = finishEvent.result if (result is CompileJavaBuildOperationType.Result) { result.annotationProcessorDetails!!.forEacha{ if (it.className.contains("dagger"))a{ daggerTime += it.executionTimeInMillis } } } } }
  135. var daggerTime = 0L val listener = object : BuildOperationListener

    { override fun finished( buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { val result = finishEvent.result if (result is CompileJavaBuildOperationType.Result) { result.annotationProcessorDetails!!.forEacha{ if (it.className.contains("dagger"))a{ daggerTime += it.executionTimeInMillis } } } } } gradle.buildFinished { manager.removeListener(listener) println("This build spent $daggerTime in Dagger annotation processors") }
  136. var daggerTime = 0L val listener = object : BuildOperationListener

    { override fun finished( buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { val result = finishEvent.result if (result is CompileJavaBuildOperationType.Result) { result.annotationProcessorDetails!!.forEacha{ if (it.className.contains("dagger"))a{ daggerTime += it.executionTimeInMillis } } } } } gradle.buildFinished { println("This build spent $daggerTime in Dagger annotation processors") }
  137. • Only measures annotationProcessor • annotationProcessor is part of JavaCompileTask

    and owned by Gradle • KAPT is not measured. It is owned by JetBrains • There is no public api
  138. So we can’t measure KAPT

  139. None
  140. But we can print out the timing

  141. plugins { kotlin("jvm") kotlin("kapt") } kapt { showProcessorTimings = true

    }
  142. :libs:periodicjobs:bundleLibResDebug UP-TO-DATE :libs:periodicjobs:bundleLibRuntimeDebug UP-TO-DATE :listenerapp:devdrawer:javaPreCompileDebug :listenerapp:devdrawer:compileDebugJavaWithJavac w: [kapt] Annotation processor

    stats: w: [kapt] dagger.internal.codegen.ComponentProcessor: total: 1808 ms, init: 488 ms, 3 round(s): 1162 ms, 157 ms, 1 ms w: [kapt] dagger.android.processor.AndroidProcessor: total: 1367 ms, init: 29 ms, 3 round(s): 1333 ms, 5 ms, 0 ms w: [kapt] com.soundcloud.lightcycle.LightCycleProcessor: total: 11 ms, init: 1 ms, 3 round(s): 10 ms, 0 ms, 0 ms :features:onboarding:compileDebugKotlin :features:onboarding:javaPreCompileDebug :features:onboarding:compileDebugJavaWithJavac
  143. None
  144. SO CLOSE

  145. It’s in the build scan!

  146. Where?

  147. val listener = object : BuildOperationListener { override fun progress(operationIdentifier:

    OperationIdentifier, progressEvent: OperationProgressEvent) { } override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { } override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) { } }
  148. val listener = object : BuildOperationListener { override fun progress(

    operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { } }
  149. LogEventBuildOperationProgressDetails

  150. @UsedByScanPlugin public interface LogEventBuildOperationProgressDetails { String getMessage(); Throwable getThrowable(); String

    getCategory(); LogLevel getLogLevel(); }
  151. val listener = object : BuildOperationListener { overrideafunaprogress( operationIdentifier: OperationIdentifier,

    progressEvent: OperationProgressEvent) { } }
  152. overrideafunaprogress( operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { val log = progressEvent.details

    if (log is LogEventBuildOperationProgressDetails) { if (log.message.contains("kapt") && log.message.contains("dagger")) { daggerTime += "\\d+".toRegex().find(log.message)!!.groups[0]!!.value.toInt() } } } w: [kapt] dagger.android.processor.AndroidProcessor: total: 1367 ms, init: 29 ms, 3 round(s): 1333 ms, 5 ms, 0 ms
  153. overrideafunaprogress( operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { val log = progressEvent.details

    if (log is LogEventBuildOperationProgressDetails) { if (log.message.contains("kapt") && log.message.contains("dagger")) { daggerTime += "\\d+".toRegex().find(log.message)!!.groups[0]!!.value.toInt() } } } w: [kapt] dagger.android.processor.AndroidProcessor: total: 1367 ms, init: 29 ms, 3 round(s): 1333 ms, 5 ms, 0 ms
  154. overrideafunaprogress( operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { val log = progressEvent.details

    if (log is LogEventBuildOperationProgressDetails) { if (log.message.contains("kapt") && log.message.contains("dagger")) { daggerTime += "\\d+".toRegex().find(log.message)!!.groups[0]!!.value.toInt() } } } w: [kapt] dagger.android.processor.AndroidProcessor: total: 1367 ms, init: 29 ms, 3 round(s): 1333 ms, 5 ms, 0 ms
  155. overrideafunaprogress( operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { val log = progressEvent.details

    if (log is LogEventBuildOperationProgressDetails) { if (log.message.contains("kapt") && log.message.contains("dagger")) { daggerTime += "\\d+".toRegex().find(log.message)!!.groups[0]!!.value.toInt() } } } w: [kapt] dagger.android.processor.AndroidProcessor: total: 1367 ms, init: 29 ms, 3 round(s): 1333 ms, 5 ms, 0 ms
  156. overrideafunaprogress( operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { val log = progressEvent.details

    if (log is LogEventBuildOperationProgressDetails) { if (log.message.contains("kapt") && log.message.contains("dagger")) { daggerTime += "\\d+".toRegex().find(log.message)!!.groups[0]!!.value.toInt() } } } w: [kapt] dagger.android.processor.AndroidProcessor: total: 1367 ms, init: 29 ms, 3 round(s): 1333 ms, 5 ms, 0 ms
  157. override fun progress( operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { val log

    = progressEvent.details if (log is LogEventBuildOperationProgressDetails) { if (log.message.contains("kapt") && log.message.contains("dagger")) { daggerTime += "\\d+".toRegex().find(log.message)!!.groups[0]!!.value.toInt() } } } gradle.buildFinished { println("This build spent $daggerTime in Dagger annotation processors") } w: [kapt] dagger.android.processor.AndroidProcessor: total: 1367 ms, init: 29 ms, 3 round(s): 1333 ms, 5 ms, 0 ms
  158. VisualVM to analyze leaks (attach to gradle, not builid)

  159. None
  160. None
  161. None
  162. None
  163. Gradle Profiler

  164. None
  165. None
  166. 68.5s average of 5 builds without Gradle Doctor 70.1s average

    of 5 builds with Gradle Doctor std deviation = 10.5 seconds Profiler results show no significant difference
  167. What else can we do?

  168. What else can we do?

  169. None
  170. Warn on slow connection speed

  171. ====================== Gradle Doctor Prescriptions ====================== | Detected a slow download

    speed downloading from Build Cache. | | 4.28 MB downloaded in 53.43 s | | Total speed from cache = 0.08 MB/s | =========================================================================
  172. Is the remote build cache worth it?

  173. Cached Size (MB) Build Duration (s) Min Connection Speed (MB/s)

    =
  174. ./gradlew :app:assembleDebug -PbenchmarkRemoteCache

  175. ========= Remote Build Cache Benchmark Report ======== Executed tasks created

    compressed artifacts of size 159,93 MB Total Task execution time was 208,85 s In order for a remote build cache to save you time, you would need an internet connection to your node of at least 0,77 MB/s. ======================================================
  176. Benchmarking the build cache medium.com/@runningcode

  177. Any information the build scan gathers!

  178. • Ensure JAVA_HOME matches • Warn on excessive Garbage collection

    • Warn if spending too much time with Dagger • Warn on slow build cache connection speed • Benchmark Remote Build cache
  179. Thanks https://github.com/runningcode/gradle-doctor osacky.com @nellyspageli nelson@osacky.com medium.com/@runningcode

  180. None