Every day hundreds of Facebook engineers make thousands of code changes, each of which requires at least one, and usually many, iterations of the edit-compile-run development cycle. To speed up this process, we built and open-sourced Buck, a build tool designed from the ground up for fast iteration, allowing engineers to compile and run changes quickly.
As our codebase and our contributor base has grown, we have continued to evolve and improve Buck. At the heart of its speed is the incremental nature of the compile step in the edit-compile-run cycle. To achieve this incrementally, Buck recompiles only the set of modules that are local to the changes that you have made. Then, instead of producing an entire APK which must be pushed to the device, Buck can push this small subset of recompiled modules.
So what about the run step? The current practice is to shut down the test app entirely and then restart it with the changed code in place. Once the app is responsive, the engineer has to reset the state of the app to return to the screen where she is working or to reproduce the bug that she is fixing. Slow time to interact (TTI) and slow app restart can slow down and add friction to the development cycle. Buck supports an additional flag to launch directly into a given activity (
buck install —activity) that can help reset some state, but it doesn’t provide assistance for single-activity apps that use a View management framework or Fragment-based applications, and it does little to address cold start times or configuring additional state, such as whether the user is logged in.
We wanted to make this last step of the cycle incremental as well, and today we’re excited to announce that we are bringing hot code reloading to Buck in the form of HotSwap. In this post we will talk about how HotSwap works, some of the performance gains that we have seen, and how it compares with similar approaches.
How it works
HotSwap combines two existing solutions in order to perform its task: Buck’s existing Exopackage support for Android development, and the Java ClassLoader.
Exopackage is Buck’s approach to incremental installation. The first time an app is installed on the device, it includes an “exoskeleton” APK that contains a minimal set of code. Buck then copies the remaining code to a temporary directory on the device, and the skeleton app will load that code after launch. This greatly speeds up incremental installation, since only the changed code needs to be copied to the temporary directory, and Buck does not need to invoke the package manager or ship a new APK to the device.
Once the code is on the device, it must be loaded by a ClassLoader. By default, ClassLoaders are chained together in a hierarchy. Whenever a class lookup occurs, for example from a new instance creation or a static method call, the initiating class will ask its own loader for the definition of the desired class. This triggers a call to loadClass, which by default is implemented as a chain of fallback searches:
- First, the loader checks its class cache to see if it has a previously loaded definition for the class.
- Next, the ClassLoader asks its parent for the class. This ensures a ClassLoader cannot load a conflicting definition for java.* or android.* classes.
- If neither of these checks produce a definition, the loader will check its dex files for the class and load it from disk.
- If all of the above steps fail, the loader will throw a ClassNotFoundException.
Ideally, we could instruct a ClassLoader to drop an entry from its cache and to read a new one from disk. In reality, there is no simple, portable way to remove an individual entry from the cache. HotSwap inserts a delegate ClassLoader into the hierarchy that can be dropped and repopulated from disk when the definitions change. Buck already inserts its own ClassLoader into the hierarchy to load class definitions from the exopackage directory, so HotSwap simply adds an additional delegate loader.
Once we put it all together, the end-to-end flow of HotSwap is as follows:
- Buck incrementally compiles the code that you’ve changed
- Buck’s exopackage support installs the changed code to the temporary directory
- The Delegate ClassLoader is thrown away, and a new one is created
- Subsequent calls to load the changed classes see the newly changed code
To benchmark the typical workflow improvement that HotSwap brings, we introduced a trivial bug into our sample app. We then fixed the bug in code, and measured the time taken from when compilation finishes to when the bug fix can be verified. Prior to HotSwap, this required a full restart of the application and some additional navigation steps to return to the screen where the bug occurred. Enabling HotSwap eliminated the restart and navigation steps, resulting in a 90% decrease in the time taken to verify the bug fix.
Deploying HotSwap to some of the debug builds of our apps resulted in our engineers enjoying faster edit-compile-run cycles.
HotSwap has a unique approach to enabling instant reloading of code inside your application. Rather than using in-memory trampolines and defining new classes at runtime, we extend incremental installation to allow incremental re-loading of code. Taking advantage of Buck’s incremental installation support means changes loaded through HotSwap are persistent between app restarts. So if you need to do a full restart, there’s no inconsistency of state between the compiled definition and what’s running in memory on the device. HotSwap also allows for more comprehensive edits. Because HotSwap performs a fresh load for the changed class definitions, any piece of the code may be edited. This includes defining new methods or fields, or even new classes.
One of the drawbacks to this approach is that it requires an activity restart in most cases. With HotSwap, the changed code is injected into the process at the next time the Class definition is requested. This means that for changes to Activity or Fragment code, a new instance must be created. Swapping of method definitions in place could be added in the future, but we have found that an Activity restart does not degrade the experience significantly.
Since HotSwap is specifically focused on reloading code inside the app, there is no support for resource changes. In the future, we should be able to add support for changing resource definitions on the fly, e.g. through a custom AssetManager.
HotSwap is still in its early days, and we are actively expanding it to work with more use cases.