Debugging and optimizing Bitrise Build Cache for Gradle

Intro

This guide is designed for users who have activated Bitrise Build Cache for Gradle (learn more here). Please note that this feature is currently in an invite-only preview stage. If you are interested in testing it, feel free to reach out to us.

Have you activated Bitrise Build Cache for Gradle but not noticed a reduction of at least 50% in your Gradle build times on your CI? This could be due to a misconfiguration that is impacting what can and cannot be cached.

Every Gradle build is composed of tasks. Examples of these are javaPreCompile, compileKotlin, or kaptKotlin. Every task has inputs and outputs. Gradle build cache saves the fingerprint of the inputs plus the output. When a new build is run, Gradle checks each task’s input fingerprint if it has a match in the cache. If there is a match, Gradle skips running the task and instead re-uses the previously saved output.

Every re-used output speeds your build up. The purpose of this document is to increase the rate at which Gradle finds matching cache entries and is able to skip tasks.

Should you require assistance in determining the appropriate configuration, please do not hesitate to reach out to us. Additionally, we have compiled a selection of tips and tricks that may guide you in adjusting your configuration independently.

Tips & tricks

What things can cause Gradle Build Cache to be ineffective

Following are things which can cause Gradle Build Cache to be ineffective, sorted based on how much work is needed for debugging and fixing the issue. Recommendation is to start from the top as those are the easiest to fix.

  • Changing the versionCode or versionName in build.gradle will most likely cause gradle to run all tests and ignore previously cached test results, and will make gradle to ignore cache for other gradle tasks as well.
  • Referencing dynamic environment variables in build configuration files (e.g. in build.gradle). Similar to how changing the versionCode or versionName in the build.gradle file, build configuration changes affect the inputs of the task and so it affects the hash used for caching.
    • From the related Gradle docs: “If the implementation is the same, then you need to start comparing inputs between the two builds. There should be at least one different input hash.”
    • A common example would be referencing the Bitrise CI build’s build number or build ID/Slug, or the git commit hash in your build config files. To maximise cache hit rate you should only do this where it’s absolutely necessary.
  • In older Android Gradle Plugins certain tasks were not cacheable (e.g. Lint wasn’t cacheable before version 3.5 - you can find more information here).
    • Solution: upgrade the Android Gradle Plugin in your project.
    • Note: unlike what’s stated in the Gradle guide Unit Tests are cacheable since AGP 3.6 (source), and Instrumented Tests are cacheable if you use Gradle Managed Devices (source).
  • The Fabric Plugin and Crashlytics: […] In practice, the default behavior of Crashlytics is to treat “each version” as synonymous with “each build”. This defeats incremental build, because each build will be unique. It also breaks the cacheability of certain tasks in the build, and for the same reason.
    • This can be fixed by simply disabling Crashlytics in “debug” builds. You may find instructions for that in the Crashlytics documentation.
  • Jetifier.
    • Disable Jetifier if you use it and you’re able to drop it. You can find a guide which explains how Jetifier affects Gradle build cache and how you can check if you can turn it off here: Disabling Jetifier.
  • Highlights from Gradle’s “Debugging and diagnosing cache misses“ guide:
    • [source] […] If the implementation is the same, then you need to start comparing inputs between the two builds. There should be at least one different input hash. If it is a simple value property, then the configuration of the task changed. This can happen for example by
      • changing the build script,
      • conditionally configuring the task differently for CI or the developer builds,
      • depending on a system property or an environment variable for the task configuration,
      • or having an absolute path which is part of the input.
  • Your app’s code isn’t modularized, or not modularized enough.
    • If your code isn’t modularized, when the code is changed you might see minimal (or no) build time improvement as Gradle will recompile the whole module and related tasks instead of being able to use the previous task execution’s results from the cache.
    • When you modularize your app, Gradle can identify which modules have been changed and only rebuild those, making the build process faster. If your app is not modularized, any small change can cause the entire app to be rebuilt. Gradle build cache stores the output of its tasks. When you modularize your app, unchanged modules can utilize the build cache, reducing the need for redundant work and speeding up the build process.
    • You can find the official Android documentation related to app modularization here: Guide to Android app modularization | Android Developers

Profile a build - to see which tasks were cached and which ones weren’t

See:

Further reading

Conclusion

You now possess the necessary tools to significantly reduce your build time. :rocket:

As always, if you have any questions please do not hesitate to reach out to us or leave a comment below.

Happy building!