Slide 1

Slide 1 text

Secrets of the Gradle Build Scan Plugin Nelson Osacky

Slide 2

Slide 2 text

Secrets of the Gradle Build Scan Plugin Nelson Osacky

Slide 3

Slide 3 text

Secrets of the Gradle Enterprise Gradle Plugin Nelson Osacky

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Me • Previously at Square in SF on Square Register and Build Tools • Currently Working at SoundCloud in Berlin

Slide 6

Slide 6 text

Me • Je parle francais ! • J'ai étudié à l’Université de Technologie de Compiègne

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

What is a build scan?

Slide 11

Slide 11 text

BUILD SUCCESSFUL in 14s 1 actionable task: 1 executed Publishing build scan... http://gradle-enterprise.dev.s-cloud.net:81/s/4ctreiklpx5da

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

What are we solving?

Slide 14

Slide 14 text

Mismatched JDK causes extra Gradle Daemon

Slide 15

Slide 15 text

Excessive GC time causes slow builds

Slide 16

Slide 16 text

Annotation processing time can be skipped with Dagger Reflect

Slide 17

Slide 17 text

Annotation processing time can be skipped with Dagger Reflect

Slide 18

Slide 18 text

Nobody checks build scans

Slide 19

Slide 19 text

Too much information

Slide 20

Slide 20 text

Build Scan Data -> Actionable Insights

Slide 21

Slide 21 text

Mismatched JDK

Slide 22

Slide 22 text

Mismatched JDK causes extra Gradle Daemon

Slide 23

Slide 23 text

Jetbrains s.r.o JDK Oracle Java SE

Slide 24

Slide 24 text

Mismatched JDK causes extra Gradle Daemon

Slide 25

Slide 25 text

Jetbrains s.r.o JDK Oracle Java SE

Slide 26

Slide 26 text

6GB 6GB 4GB

Slide 27

Slide 27 text

2 x 6GB Daemon + 4GB AS = 16GB We all have 16GB MacBooks What about other apps? Web browser?

Slide 28

Slide 28 text

How can we fix it?

Slide 29

Slide 29 text

6GB 6GB 4GB

Slide 30

Slide 30 text

Don’t use built-in JDK, use JAVA_HOME

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

Add instructions for the fix to Onboarding

Slide 34

Slide 34 text

Nobody follows instructions

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

Nobody reads warnings

Slide 37

Slide 37 text

Send everyone an email?

Slide 38

Slide 38 text

Make a presentation?

Slide 39

Slide 39 text

Walk around to teammates’ desks?

Slide 40

Slide 40 text

Let’s solve this programmatically

Slide 41

Slide 41 text

Disallow multiple Daemons by failing the build

Slide 42

Slide 42 text

1st attempt Detect multiple daemons

Slide 43

Slide 43 text

$ ps aux

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

$ ps aux | grep GradleDaemon

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

$ ps aux | grep GradleDaemon | wc -l

Slide 49

Slide 49 text

$ ps aux | grep GradleDaemon | wc -l 5

Slide 50

Slide 50 text

Gradle Time!

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

• Run command ps command • Check if more than one daemon • Display warning

Slide 53

Slide 53 text

$ ps aux | grep GradleDaemon | wc -l

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

First approach

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

2nd attempt

Slide 63

Slide 63 text

We just want JAVA_HOME to match

Slide 64

Slide 64 text

Use JAVA_HOME Use JAVA_HOME

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

val environmentJavaHomea=bSystem.getenv("JAVA_HOME") val gradleJavaHomec=dJvm.current().javaHome Gradle launched from Android studio will use AS embedded JDK by default

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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!

Slide 72

Slide 72 text

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!

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

Sharing is caring Sharing JAVA_HOME means sharing Gradle Daemon

Slide 75

Slide 75 text

No content

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

No content

Slide 78

Slide 78 text

Basic build measurements

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

overrideafunabuildFinished(result:aBuildResult)a{ } build.gradle.kts

Slide 81

Slide 81 text

gradle.buildFinished { } build.gradle.kts

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

val start = System.currentTimeMillis() gradle.buildFinished { val finished = System.currentTimeMillis() println("Build took ${finished - start}ms") } build.gradle.kts

Slide 84

Slide 84 text

To see more detail about a task, run gradlew help --task For troubleshooting, visit https://help.gradle.org Build took 288ms BUILD SUCCESSFUL in 1s 1 actionable task: 1 executed

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

ManagementFactory.getGarbageCollectorMXBeans()

Slide 87

Slide 87 text

ManagementFactory. getGarbageCollectorMXBeans()

Slide 88

Slide 88 text

/** * 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 getGarbageCollectorMXBeans() { return getPlatformMXBeans(GarbageCollectorMXBean.class); }

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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") }

Slide 94

Slide 94 text

Spent 17 ms garbage collecting BUILD SUCCESSFUL in 2s

Slide 95

Slide 95 text

Awesome!

Slide 96

Slide 96 text

Only print GC time if it is high High GC time -> Actionable

Slide 97

Slide 97 text

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") }

Slide 98

Slide 98 text

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") }

Slide 99

Slide 99 text

Build took 1257 ms Spent 17 ms garbage collecting 0.013524264 % garbage collecting BUILD SUCCESSFUL in 2s

Slide 100

Slide 100 text

=============================== 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 | ========================================================================================================

Slide 101

Slide 101 text

Java Annotation Processor events

Slide 102

Slide 102 text

Annotation processing time can be skipped with Dagger Reflect

Slide 103

Slide 103 text

No content

Slide 104

Slide 104 text

No content

Slide 105

Slide 105 text

If Gradle Enterprise can do it, so can we!

Slide 106

Slide 106 text

No content

Slide 107

Slide 107 text

No content

Slide 108

Slide 108 text

No content

Slide 109

Slide 109 text

No content

Slide 110

Slide 110 text

/** * 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(); }

Slide 111

Slide 111 text

/** * 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 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 get(Class serviceType) throws UnknownServiceException, ServiceLookupException; }

Slide 112

Slide 112 text

/** * 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(); }

Slide 113

Slide 113 text

val gradleInternal = (gradle as GradleInternal)

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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) { } })

Slide 116

Slide 116 text

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!

Slide 117

Slide 117 text

Nobody reads warnings

Slide 118

Slide 118 text

/** * 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(); }

Slide 119

Slide 119 text

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.

Slide 120

Slide 120 text

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.

Slide 121

Slide 121 text

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) { } })

Slide 122

Slide 122 text

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) { } }

Slide 123

Slide 123 text

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) } ☺

Slide 124

Slide 124 text

overrideafunafinished(buildOperation:aBuildOperationDescriptor,afinishEvent: OperationFinishEvent)a{ }

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

public final class OperationFinishEvent { long startTime; long endTime; Throwable failure; Object result; }

Slide 127

Slide 127 text

No content

Slide 128

Slide 128 text

public final class OperationFinishEvent { long startTime; long endTime; Throwable failure; Object result; }

Slide 129

Slide 129 text

@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. * *

Details are only available if an instrumented compiler was used. * * @return details about used annotation processors; {@code null} if unknown. */ @Nullable List 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(); } } }

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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 } } } } }

Slide 132

Slide 132 text

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 } } } } }

Slide 133

Slide 133 text

result.annotationProcessorDetails!!.forEacha{ if (it.className.contains("dagger"))a{ daggerTime += it.executionTimeInMillis } }

Slide 134

Slide 134 text

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 } } } } }

Slide 135

Slide 135 text

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") }

Slide 136

Slide 136 text

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") }

Slide 137

Slide 137 text

• 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

Slide 138

Slide 138 text

So we can’t measure KAPT

Slide 139

Slide 139 text

No content

Slide 140

Slide 140 text

But we can print out the timing

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

: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

Slide 143

Slide 143 text

No content

Slide 144

Slide 144 text

SO CLOSE

Slide 145

Slide 145 text

It’s in the build scan!

Slide 146

Slide 146 text

Where?

Slide 147

Slide 147 text

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) { } }

Slide 148

Slide 148 text

val listener = object : BuildOperationListener { override fun progress( operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { } }

Slide 149

Slide 149 text

LogEventBuildOperationProgressDetails

Slide 150

Slide 150 text

@UsedByScanPlugin public interface LogEventBuildOperationProgressDetails { String getMessage(); Throwable getThrowable(); String getCategory(); LogLevel getLogLevel(); }

Slide 151

Slide 151 text

val listener = object : BuildOperationListener { overrideafunaprogress( operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) { } }

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

VisualVM to analyze leaks (attach to gradle, not builid)

Slide 159

Slide 159 text

No content

Slide 160

Slide 160 text

No content

Slide 161

Slide 161 text

No content

Slide 162

Slide 162 text

No content

Slide 163

Slide 163 text

Gradle Profiler

Slide 164

Slide 164 text

No content

Slide 165

Slide 165 text

No content

Slide 166

Slide 166 text

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

Slide 167

Slide 167 text

What else can we do?

Slide 168

Slide 168 text

What else can we do?

Slide 169

Slide 169 text

No content

Slide 170

Slide 170 text

Warn on slow connection speed

Slide 171

Slide 171 text

====================== 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 | =========================================================================

Slide 172

Slide 172 text

Is the remote build cache worth it?

Slide 173

Slide 173 text

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

Slide 174

Slide 174 text

./gradlew :app:assembleDebug -PbenchmarkRemoteCache

Slide 175

Slide 175 text

========= 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. ======================================================

Slide 176

Slide 176 text

Benchmarking the build cache medium.com/@runningcode

Slide 177

Slide 177 text

Any information the build scan gathers!

Slide 178

Slide 178 text

• 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

Slide 179

Slide 179 text

Thanks https://github.com/runningcode/gradle-doctor osacky.com @nellyspageli [email protected] medium.com/@runningcode

Slide 180

Slide 180 text

No content