Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Remote Build Cache Misses and how to solve them by annoying your colleagues.

Remote Build Cache Misses and how to solve them by annoying your colleagues.

This is a talk about remote build cache misses and how we solved them at SoundCloud.
This was presented and Berlin Gradle Night on April 8, 2019.
Here is a recording of the presentation:
https://youtu.be/2frfDMJwvf4?t=384

Nelson Osacky

April 08, 2019
Tweet

More Decks by Nelson Osacky

Other Decks in Technology

Transcript

  1. Gradle Remote Cache Misses and how to solve them by

    annoying your colleagues Nelson Osacky
  2. Build Cache • Mechanism to reuse outputs from other builds

    • Allows builds to refetch outputs when it is determined that the inputs haven’t changed • Can be local (filesystem) or remote (http)
  3. • The task type and its classpath • The names

    of the output properties • The names and values of properties annotated as described in the section called "Custom task types" • The names and values of properties added by the DSL via TaskInputs • The classpath of the Gradle distribution, buildSrc and plugins • The content of the build script when it affects execution of the task
  4. Local Cache If you have built it before, skip building

    it again. Remote Cache If anyone else has built it before, skip building it again.
  5. Local CI In order to re-use task outputs we need

    to have the same build cache key across environments.
  6. Identify what changed between two executions of a task that

    prevented the output from being reused from a build cache.
  7. public final class BuildConfig { public static final boolean DEBUG

    = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.soundcloud.android"; public static final String BUILD_TYPE = "debug"; public static final int VERSION_CODE = -1; public static final int TEST_RETRY_COUNT = 0; }
  8. public final class BuildConfig { public static final boolean DEBUG

    = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.soundcloud.android"; public static final String BUILD_TYPE = "debug"; public static final int VERSION_CODE = -1; public static final int TEST_RETRY_COUNT = 0; }
  9. afterEvaluate { android.libraryVariants.all { variant -> variant.resValue "bool", "analytics_enabled", "true"

    variant.resValue "bool", "verbose_logging", "false" variant.buildConfigField "int", "TEST_RETRY_COUNT", "${getRetryCount()}" } }
  10. public final class BuildConfig { public static final boolean DEBUG

    = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.soundcloud.android"; public static final String BUILD_TYPE = "debug"; public static final int VERSION_CODE = -1; public static final int TEST_RETRY_COUNT = 0; }
  11. public final class BuildConfig { public static final boolean DEBUG

    = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.soundcloud.android"; public static final String BUILD_TYPE = "debug"; public static final int VERSION_CODE = -1; public static final int TEST_RETRY_COUNT = 1; }
  12. afterEvaluate { android.libraryVariants.all { variant -> variant.resValue "bool", "analytics_enabled", "true"

    variant.resValue "bool", "verbose_logging", "false" variant.buildConfigField "int", "TEST_RETRY_COUNT", "${getRetryCount()}" } }
  13. public final class BuildConfig { public static final boolean DEBUG

    = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.soundcloud.android"; public static final String BUILD_TYPE = "debug"; public static final int VERSION_CODE = -1; public static final int TEST_RETRY_COUNT = 1; }
  14. public final class BuildConfig { public static final boolean DEBUG

    = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.soundcloud.android"; public static final String BUILD_TYPE = "debug"; public static final int VERSION_CODE = -1; }
  15. open class ActivityTest<T : Activity> protected constructor(activityClass: Class<T>) { private

    val isRunningOnTestLab = Settings.System.getString(contentResolver, "firebase.test.lab") == "true" @JvmField val retryRule = RetryRule(if (isRunningOnTestLab) 1 else 0) }
  16. tasks.withType(SourceTask).configureEach {at -> t.doFirst { t.source.visit { FileVisitDetails d ->

    if (d.file.directory && d.file.listFiles().size() == 0) { throw new IllegalStateException( "Found an empty source directory. This causes build cache misses. Please remove it manually. rmdir " + d.file.absolutePath) } } } }
  17. tasks.withType(SourceTask).configureEach {at -> t.doFirst {a t.source.visit {aFileVisitDetails d ->a if

    (d.file.directory && d.file.listFiles().size() == 0) { throw new IllegalStateException( "Found an empty source directory. This causes build cache misses. Please remove it manually. rmdir " + d.file.absolutePath) } } } }
  18. tasks.withType(SourceTask).configureEach { t -> t.doFirst {a t.source.visit {aFileVisitDetails d ->a

    if (d.file.directory && d.file.listFiles().size() == 0) {b throw new IllegalStateException( "Found an empty source directory. This causes build cache misses. Please remove it manually. rmdir " + d.file.absolutePath) }c }d } }
  19. t.source.visit {aFileVisitDetails d ->a if (d.file.directory && d.file.listFiles().size() == 0)

    {b throw new IllegalStateException( "Found an empty source directory. This causes build cache misses. Please remove it manually. rmdir " + d.file.absolutePath) }c }d
  20. if (d.file.directory && d.file.listFiles().size() == 0) {b throw new IllegalStateException(

    "Found an empty source directory. This causes build cache misses. Please remove it manually. rmdir " + d.file.absolutePath) }c
  21. tasks.withType(SourceTask).configureEach { t -> t.doFirst { t.source.visit { FileVisitDetails d

    -> if (d.file.directory && d.file.listFiles().size() == 0) { throw new IllegalStateException( "Found an empty source directory. This causes build cache misses. Please remove it manually. rmdir " + d.file.absolutePath) }c }d } }
  22. !"" build.gradle !"" buildSrc # !"" build.gradle # $"" src

    # !"" main # $"" java # $"" com # $"" enterprise # !"" Deploy.java # $"" DeploymentPlugin.java $"" settings.gradle
  23. • All classes that are visible to the task class

    are considered part of the implementation. • Usually a transient cache miss when adding or removing a plugin • Also a transient miss happens when modifying buildSrc
  24. tasks.named("compileDebugAidl").configure {a doLast {b outputs.files.forEach { directory -> directory.traverse(type: FILES)

    { file -> file.setText((file as String[]).findAll { !it.contains('Original file:') }.join(‚\n'), 'utf-8') } } } }
  25. tasks.named("compileDebugAidl").configure {a doLast {b outputs.files.forEach {adirectory -> directory.traverse(type: FILES) {bfile

    -> file.setText((file as String[]).findAll { !it.contains('Original file:') }.join(‚\n'), 'utf-8') } } } }
  26. tasks.named("compileDebugAidl").configure {a doLast {b outputs.files.forEach {adirectory -> directory.traverse(type: FILES) {bfile

    -> file.setText((file as String[]).findAll {d !it.contains('Original file:') }.join(‚\n'), 'utf-8') } } } }
  27. tasks.named("compileDebugAidl").configure { doLast { outputs.files.forEach { directory -> directory.traverse(type: FILES)

    { file -> file.setText((file as String[]).findAll {d !it.contains('Original file:') }.join(‚\n'), 'utf-8') } } } }
  28. • PathSensitivity.ABSOLUTE • Consider the full paths of files and

    directories. • PathSensitivity.RELATIVE • Use the location of the file relative to a hierarchy.
  29. • If a Task declares a file property without PathSensivity

    annotation, the default is PathSensivity.ABSOLUTE.
  30. 25% faster builds on avg (49.55s - 36.84s) / 49.55s

    = 1000+ builds / week ~12s / build