This is a guest post by Pavlos-Petros Tournaris. It originally appeared on his blog here. Pavlos-Petros works as an Android software engineer at Workable. We hope you enjoy!
Nearly a year and a half ago, Facebook released Litho as an open source project. Litho is an Android framework, which lets you define your UI in a declarative way. It immediately got my interest and I started getting my hands dirty with some examples and pet projects. It was a nice experience getting in touch with Litho and its React-like nature, that was a first for me. The amount of interest in that new area, made me realize that I could dive a bit more into it, by also contributing to the project. So just like with any other open source project that I like, I started checking the “Issues” tab to see if I could help resolving any bugs or contributing new features.
Automated testing
Litho, just like the majority of open source projects, includes a lot of tests (without tests it is usually hard to gain a developer’s trust). Testing infrastructure on Litho is based on unit tests that are run either through Buck or using Gradle.
But tests alone don’t mean anything if you are not able to have proper feedback on your PRs and also make sure each commit that goes into master
branch is green.
Litho was using CircleCI to leverage the run of its unit test suite, gather test results, and publish a snapshot for each commit pushed on master
.
Previous situation
Previously, Litho was using a custom Docker image made by Pascal Hartig, one of Litho’s Android engineers.
This image was downloading Buck, building it and saving it in the Docker environment. The same was done for Android NDK and Android SDK respectively. When CircleCI started a build for a commit, it would then configure some needed keys for archives uploading and also exporting Buck into Path. Finally, it started executing all BUCK & Gradle builds for sample projects, as well as executing Buck & Gradle tests, which was finalized by publishing a snapshot and storing test results.
While this worked great, there was the need to configure parallel jobs for Buck and Gradle tests and sample builds.
Problems with the previous situation
While starting to investigating what could be improved, there were certain things I noticed, that could be more performant and others that came up after discussing them with Pascal.
- CircleCI configuration was using a custom Docker image that was effectively pulled each time the job was running, due to how CircleCI caches Docker images on its containers, costing around 5–6 mins of spinning an environment.
-
Builds & tests were not running in parallel, which meant that even if a Buck build required two minutes to execute tests, it would have to wait for Gradle to also finish executing tests before proceeding.
-
The regex used to collect test results was not collecting Buck test results.
Here comes Workflows
CircleCI provides a feature called Workflows, which allows us to define a list of jobs that will run in parallel, but you can also declare dependencies between jobs, which would effectively change their execution to be sequential.
- checkout_code
- build:
requires:
- checkout_code
- buck_sample_build:
requires:
- build
- buck_sample_barebones_build:
requires:
- build
- buck_sample_codelab_build:
requires:
- build
- gradle_sample_kotlin_build:
requires:
- build
- buck_litho_it_tests_run:
requires:
- build
- buck_litho_it_powermock_tests_run:
requires:
- build
- gradle_tests_run:
requires:
- build
Transitioning Litho to use Workflows
The first step was to create a job that would check out the repo code and start setting up required dependencies, including: Buck, dependencies that will help us build Buck, such as Ant, and Android NDK. This job was defined as a first step on Litho’s build_and_test
workflow.
Depending on that job is the build
job, which is responsible for building Litho and saving any Gradle-produced caches.
Afterwards, build is fanning out to seven other jobs responsible to build samples and execute tests. Each one of these jobs is also configured to store test results and upload any produced artifacts.
Fanning out
This workflow is finalized by a publish_snapshot
job which is depending on every job that executes tests. After their successful execution, a Gradle task is responsible to upload an archive on Bintray containing the latest changes, as a snapshot.
- publish_snapshot:
requires:
- buck_litho_it_tests_run
- buck_litho_it_powermock_tests_run
- gradle_tests_run
Caches & Workspace
CircleCI’s config DSL, includes amongst others, cache
& workspace
. In order to better explain those terms, let me define them as follows:
cache
is saved content that will be retained between workflow executions.
Caching in CircleCI
- &save-repo-cache
paths:
- ~/.gradle/caches
- ~/.gradle/wrapper
workspace
is saved content that will be retained among jobs executions of the same workflow.
Saving content with workspaces
- persist_to_workspace:
root: workspace
paths:
- repo
For example, something that would be worth caching would be any Gradle-cached dependencies, provided that there is a proper cache-busting mechanism in place. Whereas our repo’s code is something that all jobs need so it’s worth persisting it to our workspace in order for every job to be able to “attach” and get access to that content.
attach_workspace: &attach_workspace
attach_workspace:
at: ~/litho-working-dir/workspace
Job configuration
Jobs that are part of a workflow can also define filters
for branches. This is especially helpful, since we would not like to execute publish_snapshot
job in case the commit we were running on is not on master
branch, but rather a PR. This might seem like a small change but it can save 7-8 minutes of waiting for the workflow’s execution to finish.
filters:
branches:
only: master
Post-restructure improvements
After a lot of commits, workflow executions and discussions with Pascal on improvable points, we finally managed to have a workflow running in around 15–20 minutes. Here are some points that we improved:
- We removed the custom Docker image by using CircleCI’s Android image and pulling any needed dependencies on start-up. Gave us a 1sec container configuration for 95% (or even more) of job executions.
- We used CircleCI’s Android Docker image Android SDK, instead of downloading from scratch.
- We made test & build jobs run in parallel, allowing for early feedback in case something is broken only on a “sample” module for example. Combined with Github’s integration this is really informative, because you can instantly be notified if a Job responsible to execute tests for your changes, has failed or not, without having to wait for the whole workflow to finish.
- We collected previously uncollected test results from Buck test executions.
Conclusion
It has been a fun experience and eye-opening experience. I have never dealt with CI tools in that extent before and I can totally say that DevOps or CI provisioning is a certainly a difficult job (respect to our colleagues out there who have to deal with that every day).
Also, thanks to Pascal Hartig for helping on the process and discussing points of improvement.
You can find the commit that restructures config.yml here: https://github.com/facebook/litho/commit/2c411830d11d08fc2af194d8ea244d0cbdf33f76
And Litho’s final config.yml here: https://github.com/facebook/litho/blob/master/.circleci/config.yml