Attacking Build Times With Sample Apps
Some problems are purely technical, and don’t directly solve any customer issue. Many problems that arise when working on large codebases fall into that category.
From the point of view of maintainability, extensibility, speed, and who knows what other measures, a small codebase is preferable to a large one. If a large codebase could magically be turned into a small one, this would almost without exception be the right move.
In the happy event that this magical technique is invented, we can throw this blog post directly into the garbage can. Until then, let’s talk about how to attack the problem of build times on large codebases. The following applies to Android (my domain of choice) codebases with over, say, 200kloc., but will also apply to codebases on other platforms at different sizes. I’ll start with a high level overview of the kinds of tactics used to directly tackle a long build time, and then I’ll address a technique that is new to me in some more detail: the sample app.
Build Times: The Head On Front
The first category of solutions I’ve been party to come from what I’ll call the “head on” approach.
The head on approach is simply to reduce the total incremental build time for the application to the point where it can be used for iterative development on the same scale as a small application.
You can do this with two kinds of tactics. The first is to attack it with the build tools themselves. Many companies small and large migrate from gradle to buck or bazel, but the tooling journey can go even further than that. You can customize Buck itself, or you may build tooling to coordinate intermediate build products. This can even lead to process changes for the developer. You might build on a server instead of your own machine. What about source control, branch changes? Deep this rabbit hole goes, yes.
This rabbit hole has a big downside: you pay all the penalties of being a special snowflake, up to and including not being able to shoot the breeze with the same people about build tooling at Google I/O.
A second category of tactics is in the codebase itself. Modularization is usually necessary: a well-modularized codebase will build faster than a monolithic codebase, because you can parallelize the build and reuse incremental build products.
The tools used in the code affect this as well. An ill-behaved annotation processor could tank your build. The compiler itself can be a cost: I would be a fool to try and enumerate Kotlin’s many merits over Java, but compile time is not one of them.
Regardless of what tactics you apply, attacking build times “head on” places the bar for success high. Getting the build down from eight minutes to five minutes may be an impressive feat, but to the engineer iterating a single screen to perfection, it still counts as a failure.
Sample Apps: The Vulnerable Flank
On Cash App, I’ve been using sample apps to iterate quickly. This is a new approach to me, and one I’ve come to enjoy as a discipline. A lot of the following is received wisdom from my colleagues (including John Rodriguez and Aleksei Zakharov), but I also have some battle scars from previous industry experience.
Here’s the idea of the sample app: instead of iterating in the main app, create and deploy a separate app off to the side. Use it to exercise components like views in isolation from the rest of the app, and use unit tests and the like for more granular validations.
In addition to the obvious benefits of this approach, I appreciate that sample apps preserve the “tinkerability” of the application. I can take a single piece of the app and put it through its paces on an actual device or emulator and see it work.
Now, for the sample app approach to work at all, the app has to be modularized already. If a
View cannot display a
Banana without pulling in the gorilla and the entire jungle (as the aphorism goes), then there will be no wins here. Don’t even try. If you can, though, the sample app is a pleasant discipline.
It is a discipline, though. And if your sample app can’t abide by the discipline, it will fail and become a drag on the codebase.
Keep the dependency surface of the sample app small, small, small. As a rule of thumb, do not include any internal dependencies you do not own. A sample app with a raft of dependencies will quickly turn into cruft and start to rot.
Every single sample app is an additional build target. That’s a penalty: it’s one more thing to break that has to be tested independently. If you limit the sample app to dependencies you are responsible for, then you’ve made it much less likely that work on an unrelated part of the app will break your new special snowflake.
There’s no getting around that additional build target. Every additional dependency will expand the surface of what can break. If your own dependencies conform to a common API that might be subject to change, even those might be risky.
Keep It Simple
Every sample app should start its life with the minimum viable bits of code to be deployable: a manifest, an activity, and that’s it. Only add to that what you need to survive.
Complex things break. The more complex the sample app is, the more quickly it will turn into unmaintained cruft, and eventually become a target for deletion.
There will be good reasons to make the sample app complicated. The temptation to introduce some frameworkiness, for example, will be strong, since you’ll be doing similar things in each sample app. Resist this urge as much as is possible, because it will tie all of these ostensibly separate sample apps together. Woe betide the engineer validating their refactoring against all those build targets!
This is also a pressure to avoid frameworkiness in your own components that you’re exercising within the sample app: if the framework changes, then all the sample apps need to change, too.
Validation will be a temptation as well. The more “real” the sample app is, the more useful it seems to be. At some point, though, the bullet will have to be bit, and the validation will need to be run in the real application.
Sample Apps: Our Dearest, Dearest, Non-Codependent Friend
Sample apps will never be the primary approach for addressing poor build times. I’m currently doing feature work, which is always at the edge of the system, and thus is ideal for this kind of thing. This gets harder and harder closer to the core. In those cases, sample apps may offer no further advantage above and beyond unit tests, though I’d like to be proven wrong.
As such, sample apps are always going to be a flanking attack on the build time problem. By themselves, they will fall short, but they can ease the standard for success on the main front.
Keep it simple, limit dependencies. Don’t build frameworks, and don’t build cruft.